rulesync 38.0.0 → 39.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 +4 -4
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CoFTCNq7.js → import-CfVOkld5.js} +2187 -87
- package/dist/{import-CoFTCNq7.js.map → import-CfVOkld5.js.map} +1 -1
- package/dist/{import-B9NOtObR.cjs → import-HHSMWQi-.cjs} +2203 -103
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +7 -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$439 } = 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$439(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$439(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
473
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
475
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
476
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
477
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
478
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
479
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
480
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
481
|
+
join$439(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
482
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
483
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$439(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$439(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$439(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
489
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$439(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",
|
|
@@ -3780,6 +3793,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3780
3793
|
".devin/config.json",
|
|
3781
3794
|
".dsh/cordis.patch.yml",
|
|
3782
3795
|
".factory/settings.json",
|
|
3796
|
+
"firebender.json",
|
|
3797
|
+
".firebender/firebender.json",
|
|
3783
3798
|
".grok/config.toml",
|
|
3784
3799
|
".grok/sandbox.toml",
|
|
3785
3800
|
".letta/settings.json",
|
|
@@ -4241,14 +4256,103 @@ function stringifyFrontmatter(body, frontmatter, options) {
|
|
|
4241
4256
|
} } });
|
|
4242
4257
|
return gray_matter.default.stringify(file, cleanFrontmatter);
|
|
4243
4258
|
}
|
|
4259
|
+
/**
|
|
4260
|
+
* Thrown in place of gray-matter's built-in JavaScript engine.
|
|
4261
|
+
*
|
|
4262
|
+
* gray-matter picks the parser from the text after the opening fence, so a
|
|
4263
|
+
* file that opens with `---js` or `---javascript` has its frontmatter handed
|
|
4264
|
+
* to `eval()`. Frontmatter here comes from third-party skills, fetched sources
|
|
4265
|
+
* and untrusted repositories, so that would run arbitrary code on the machine
|
|
4266
|
+
* of whoever generates or imports it. Every engine name that resolves to code
|
|
4267
|
+
* execution is mapped to this refusal; {@link assertYamlFrontmatterLanguage}
|
|
4268
|
+
* refuses those fences earlier still, so this is the second line of defense.
|
|
4269
|
+
*/
|
|
4270
|
+
function refuseExecutableFrontmatter() {
|
|
4271
|
+
throw new Error("Frontmatter written in an executable language (JavaScript / CoffeeScript) is not supported; use YAML");
|
|
4272
|
+
}
|
|
4273
|
+
const REFUSED_FRONTMATTER_ENGINE = {
|
|
4274
|
+
parse: refuseExecutableFrontmatter,
|
|
4275
|
+
stringify: refuseExecutableFrontmatter
|
|
4276
|
+
};
|
|
4277
|
+
/**
|
|
4278
|
+
* Engines passed to every gray-matter parse. Supplying them merges over
|
|
4279
|
+
* gray-matter's defaults, so the JavaScript engine (and the CoffeeScript names
|
|
4280
|
+
* a plugin could register) can never be reached through an alias.
|
|
4281
|
+
*
|
|
4282
|
+
* `patches/gray-matter@4.0.3.patch` also deletes the JavaScript engine, but a
|
|
4283
|
+
* pnpm patch only applies inside this repository: gray-matter is an external
|
|
4284
|
+
* dependency of the published package, so an installed rulesync runs the
|
|
4285
|
+
* unpatched gray-matter, and this override plus the language check are what
|
|
4286
|
+
* protect it. Exported for the test that pins that.
|
|
4287
|
+
*/
|
|
4288
|
+
const FRONTMATTER_PARSE_OPTIONS = { engines: {
|
|
4289
|
+
javascript: REFUSED_FRONTMATTER_ENGINE,
|
|
4290
|
+
js: REFUSED_FRONTMATTER_ENGINE,
|
|
4291
|
+
coffee: REFUSED_FRONTMATTER_ENGINE,
|
|
4292
|
+
coffeescript: REFUSED_FRONTMATTER_ENGINE,
|
|
4293
|
+
cson: REFUSED_FRONTMATTER_ENGINE
|
|
4294
|
+
} };
|
|
4295
|
+
/** Frontmatter languages accepted after the opening `---` fence. */
|
|
4296
|
+
const ALLOWED_FRONTMATTER_LANGUAGE_PATTERN = /^(?:ya?ml)?$/i;
|
|
4297
|
+
/**
|
|
4298
|
+
* Read the opening frontmatter fence the way gray-matter does, or return
|
|
4299
|
+
* `undefined` when gray-matter would see no frontmatter at all.
|
|
4300
|
+
*
|
|
4301
|
+
* gray-matter strips one byte order mark, treats `---` as a fence only when a
|
|
4302
|
+
* fourth `-` does not follow, and takes the language from the text between the
|
|
4303
|
+
* fence and the first `\r?\n`, trimmed — dropping the last character when
|
|
4304
|
+
* there is no line break at all. Every check below that has to agree with
|
|
4305
|
+
* gray-matter on where the block is (the language check, the size guard, the
|
|
4306
|
+
* repair pass) goes through this one reading, so none of them can be bypassed
|
|
4307
|
+
* by a fence line gray-matter accepts and a stricter pattern would not (a
|
|
4308
|
+
* stray `\r` before the line break, say).
|
|
4309
|
+
*/
|
|
4310
|
+
function readFrontmatterFence(content) {
|
|
4311
|
+
const offset = content.startsWith("") ? 1 : 0;
|
|
4312
|
+
const text = content.slice(offset);
|
|
4313
|
+
if (!text.startsWith("---") || text.charAt(3) === "-") return;
|
|
4314
|
+
const afterFence = text.slice(3);
|
|
4315
|
+
const lineBreak = /\r?\n/.exec(afterFence);
|
|
4316
|
+
const language = afterFence.slice(0, lineBreak ? lineBreak.index : -1).trim();
|
|
4317
|
+
const lineBreakStart = lineBreak ? offset + 3 + lineBreak.index : void 0;
|
|
4318
|
+
return {
|
|
4319
|
+
language,
|
|
4320
|
+
lineBreakStart,
|
|
4321
|
+
lineEnd: lineBreak && lineBreakStart !== void 0 ? lineBreakStart + lineBreak[0].length : void 0
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
/**
|
|
4325
|
+
* Refuse a frontmatter fence that names any language other than YAML.
|
|
4326
|
+
*
|
|
4327
|
+
* Only YAML is ever written by rulesync or by the tools it targets, so anything
|
|
4328
|
+
* else — `---js` above all, whose block gray-matter would `eval()` — is
|
|
4329
|
+
* rejected before gray-matter sees the content.
|
|
4330
|
+
*/
|
|
4331
|
+
function assertYamlFrontmatterLanguage(content) {
|
|
4332
|
+
const fence = readFrontmatterFence(content);
|
|
4333
|
+
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`);
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* The number of characters gray-matter would hand to the YAML parser as the
|
|
4337
|
+
* frontmatter block. Unlike {@link findFrontmatterBlockBounds}, a missing
|
|
4338
|
+
* closing fence does not make this zero: gray-matter then parses everything
|
|
4339
|
+
* after the fence line, so that is what the size guard has to measure.
|
|
4340
|
+
*/
|
|
4341
|
+
function measureFrontmatterBlockChars(content) {
|
|
4342
|
+
const fence = readFrontmatterFence(content);
|
|
4343
|
+
if (fence?.lineBreakStart === void 0) return 0;
|
|
4344
|
+
const rest = content.slice(fence.lineBreakStart);
|
|
4345
|
+
const closeIndex = rest.indexOf("\n---");
|
|
4346
|
+
return closeIndex === -1 ? rest.length : closeIndex;
|
|
4347
|
+
}
|
|
4244
4348
|
function parseFrontmatter(content, filePath) {
|
|
4245
4349
|
let frontmatter;
|
|
4246
4350
|
let body;
|
|
4247
4351
|
let hasFrontmatter;
|
|
4248
4352
|
try {
|
|
4249
|
-
|
|
4250
|
-
if (
|
|
4251
|
-
const result = (0, gray_matter.default)(content,
|
|
4353
|
+
assertYamlFrontmatterLanguage(content);
|
|
4354
|
+
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)`);
|
|
4355
|
+
const result = (0, gray_matter.default)(content, FRONTMATTER_PARSE_OPTIONS);
|
|
4252
4356
|
frontmatter = deepRemoveNullishObject(result.data);
|
|
4253
4357
|
body = result.content;
|
|
4254
4358
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
@@ -4307,22 +4411,28 @@ function repairFrontmatterLine(line) {
|
|
|
4307
4411
|
}
|
|
4308
4412
|
/**
|
|
4309
4413
|
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
4310
|
-
* without parsing it
|
|
4311
|
-
*
|
|
4312
|
-
*
|
|
4313
|
-
*
|
|
4314
|
-
*
|
|
4315
|
-
*
|
|
4414
|
+
* without parsing it, for the YAML repair pass below and the rule loaders that
|
|
4415
|
+
* pre-process `globs:`. The opening fence is read exactly as gray-matter reads
|
|
4416
|
+
* it ({@link readFrontmatterFence}); an explicit `yaml` / `yml` language is
|
|
4417
|
+
* still YAML, any other language yields no bounds. gray-matter ends the block
|
|
4418
|
+
* at the first `\n---`, with no requirement that the delimiter be alone on its
|
|
4419
|
+
* line, so a stricter pattern here would run past gray-matter's delimiter and
|
|
4420
|
+
* act on text that is really the body. A block with no closing fence has no
|
|
4421
|
+
* bounds; the size guard measures that case separately.
|
|
4316
4422
|
*/
|
|
4317
4423
|
function findFrontmatterBlockBounds(content) {
|
|
4318
|
-
const
|
|
4319
|
-
if (!
|
|
4320
|
-
const blockStart =
|
|
4321
|
-
const closing = /\r?\n---/.exec(content.slice(
|
|
4424
|
+
const fence = readFrontmatterFence(content);
|
|
4425
|
+
if (fence?.lineBreakStart === void 0 || fence.lineEnd === void 0 || !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) return;
|
|
4426
|
+
const blockStart = fence.lineEnd;
|
|
4427
|
+
const closing = /\r?\n---/.exec(content.slice(fence.lineBreakStart));
|
|
4322
4428
|
if (!closing) return;
|
|
4429
|
+
if (closing.index === 0) return {
|
|
4430
|
+
blockStart,
|
|
4431
|
+
blockEnd: blockStart
|
|
4432
|
+
};
|
|
4323
4433
|
return {
|
|
4324
4434
|
blockStart,
|
|
4325
|
-
blockEnd:
|
|
4435
|
+
blockEnd: fence.lineBreakStart + closing.index
|
|
4326
4436
|
};
|
|
4327
4437
|
}
|
|
4328
4438
|
/**
|
|
@@ -4354,8 +4464,9 @@ function repairMalformedFrontmatterYaml(content) {
|
|
|
4354
4464
|
* narrow — one pass, top-level entries only, and the original error is what
|
|
4355
4465
|
* surfaces if it does not help, so a genuinely broken file still fails with the
|
|
4356
4466
|
* message that describes what is actually wrong with it. A file with no closing
|
|
4357
|
-
*
|
|
4358
|
-
*
|
|
4467
|
+
* `---` is not repaired at all, and one whose opening fence names a language
|
|
4468
|
+
* other than YAML is refused outright (an explicit `---yaml` / `---yml` fence
|
|
4469
|
+
* is repaired like a plain one).
|
|
4359
4470
|
*
|
|
4360
4471
|
* @see https://agentskills.io/client-implementation/adding-skills-support
|
|
4361
4472
|
*/
|
|
@@ -4720,6 +4831,29 @@ const CURSOR_HOOK_EVENTS = [
|
|
|
4720
4831
|
"workspaceOpen"
|
|
4721
4832
|
];
|
|
4722
4833
|
/**
|
|
4834
|
+
* Hook events supported by Firebender.
|
|
4835
|
+
*
|
|
4836
|
+
* Firebender's `hooks.json` follows Cursor's layout, and every event it fires
|
|
4837
|
+
* is spelled like its canonical counterpart, so no name mapping is needed.
|
|
4838
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
4839
|
+
*/
|
|
4840
|
+
const FIREBENDER_HOOK_EVENTS = [
|
|
4841
|
+
"sessionStart",
|
|
4842
|
+
"sessionEnd",
|
|
4843
|
+
"subagentStart",
|
|
4844
|
+
"subagentStop",
|
|
4845
|
+
"preToolUse",
|
|
4846
|
+
"postToolUse",
|
|
4847
|
+
"beforeShellExecution",
|
|
4848
|
+
"afterShellExecution",
|
|
4849
|
+
"beforeMCPExecution",
|
|
4850
|
+
"afterMCPExecution",
|
|
4851
|
+
"beforeReadFile",
|
|
4852
|
+
"afterFileEdit",
|
|
4853
|
+
"preCompact",
|
|
4854
|
+
"stop"
|
|
4855
|
+
];
|
|
4856
|
+
/**
|
|
4723
4857
|
* Hook events supported by Claude Code.
|
|
4724
4858
|
*
|
|
4725
4859
|
* Covers the full documented event surface.
|
|
@@ -6067,6 +6201,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
6067
6201
|
version: zod_mini.z.optional(zod_mini.z.number()),
|
|
6068
6202
|
hooks: canonicalHooksRecordSchema,
|
|
6069
6203
|
cursor: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6204
|
+
firebender: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6070
6205
|
claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6071
6206
|
copilot: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6072
6207
|
copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -7170,7 +7305,7 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
7170
7305
|
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7171
7306
|
crushChannelEnabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7172
7307
|
crushChannelReply: zod_mini.z.optional(CrushChannelReplySchema),
|
|
7173
|
-
zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7308
|
+
zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
7174
7309
|
poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7175
7310
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
7176
7311
|
/**
|
|
@@ -7272,6 +7407,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
7272
7407
|
devin: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7273
7408
|
dsh: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7274
7409
|
factorydroid: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7410
|
+
firebender: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7275
7411
|
gitlabduo: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7276
7412
|
goose: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7277
7413
|
grokcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -7296,6 +7432,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
7296
7432
|
vibe: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7297
7433
|
warp: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7298
7434
|
warpcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7435
|
+
xum: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7299
7436
|
zed: zod_mini.z.optional(toolScopedMcpSchema)
|
|
7300
7437
|
});
|
|
7301
7438
|
/**
|
|
@@ -13091,6 +13228,35 @@ const CRUSH_CONFIG_DECLARATION = {
|
|
|
13091
13228
|
}
|
|
13092
13229
|
};
|
|
13093
13230
|
/**
|
|
13231
|
+
* Firebender's `firebender.json`, shared by the project (`<project>/firebender.json`)
|
|
13232
|
+
* and personal (`~/.firebender/firebender.json`) spellings so a policy edit
|
|
13233
|
+
* lands on both. The file also carries `rules`, `commands`, `mcpEnvFile`,
|
|
13234
|
+
* `worktrees` and `useCursorRules` keys rulesync does not own, so every writer
|
|
13235
|
+
* refuses to read-modify-write a file it could not parse. Each feature owns
|
|
13236
|
+
* one whole top-level key: `ignore` (recomputed from `.rulesync/.aiignore`),
|
|
13237
|
+
* `mcpServers` (the complete server map) and `agents` (recomputed from the
|
|
13238
|
+
* existing list, with the entries for rulesync's own agent directory replaced).
|
|
13239
|
+
* https://docs.firebender.com/api-reference/syntax
|
|
13240
|
+
*/
|
|
13241
|
+
const FIREBENDER_CONFIG_DECLARATION = {
|
|
13242
|
+
format: "json",
|
|
13243
|
+
invalidRootPolicy: "error",
|
|
13244
|
+
features: {
|
|
13245
|
+
ignore: {
|
|
13246
|
+
kind: "replace-owned-keys",
|
|
13247
|
+
ownedKeys: ["ignore"]
|
|
13248
|
+
},
|
|
13249
|
+
subagents: {
|
|
13250
|
+
kind: "replace-owned-keys",
|
|
13251
|
+
ownedKeys: ["agents"]
|
|
13252
|
+
},
|
|
13253
|
+
mcp: {
|
|
13254
|
+
kind: "replace-owned-keys",
|
|
13255
|
+
ownedKeys: ["mcpServers"]
|
|
13256
|
+
}
|
|
13257
|
+
}
|
|
13258
|
+
};
|
|
13259
|
+
/**
|
|
13094
13260
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
13095
13261
|
* and both validate against the one published schema, which the gateway
|
|
13096
13262
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13330,6 +13496,15 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13330
13496
|
ownedKeys: ["servers"]
|
|
13331
13497
|
} }
|
|
13332
13498
|
},
|
|
13499
|
+
".xum/mcp.jsonc": {
|
|
13500
|
+
format: "jsonc",
|
|
13501
|
+
invalidRootPolicy: "error",
|
|
13502
|
+
jsoncParseErrors: "error",
|
|
13503
|
+
features: { mcp: {
|
|
13504
|
+
kind: "replace-owned-keys",
|
|
13505
|
+
ownedKeys: ["servers"]
|
|
13506
|
+
} }
|
|
13507
|
+
},
|
|
13333
13508
|
".qwen/settings.json": {
|
|
13334
13509
|
format: "json",
|
|
13335
13510
|
features: {
|
|
@@ -13417,6 +13592,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13417
13592
|
".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
|
|
13418
13593
|
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13419
13594
|
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13595
|
+
"firebender.json": FIREBENDER_CONFIG_DECLARATION,
|
|
13596
|
+
".firebender/firebender.json": FIREBENDER_CONFIG_DECLARATION,
|
|
13420
13597
|
".bob/settings.json": {
|
|
13421
13598
|
format: "json",
|
|
13422
13599
|
invalidRootPolicy: "error",
|
|
@@ -20251,6 +20428,178 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
20251
20428
|
}
|
|
20252
20429
|
};
|
|
20253
20430
|
//#endregion
|
|
20431
|
+
//#region src/constants/firebender-paths.ts
|
|
20432
|
+
/**
|
|
20433
|
+
* Firebender (the coding agent plugin for Android Studio and other JetBrains
|
|
20434
|
+
* IDEs) keeps project configuration under `.firebender/` plus a project-root
|
|
20435
|
+
* `firebender.json`, and user configuration under `~/.firebender/` (including
|
|
20436
|
+
* `~/.firebender/firebender.json`).
|
|
20437
|
+
*
|
|
20438
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
20439
|
+
* @see https://docs.firebender.com/api-reference/rules
|
|
20440
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20441
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
20442
|
+
* @see https://docs.firebender.com/api-reference/skills
|
|
20443
|
+
* @see https://docs.firebender.com/api-reference/agents-md
|
|
20444
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
20445
|
+
*/
|
|
20446
|
+
const FIREBENDER_DIR = ".firebender";
|
|
20447
|
+
/**
|
|
20448
|
+
* `firebender.json`: at the project root for project scope, and under
|
|
20449
|
+
* `~/.firebender/` for the personal config. It is shared by the ignore
|
|
20450
|
+
* (`ignore`), MCP (`mcpServers`) and subagents (`agents`) features.
|
|
20451
|
+
*/
|
|
20452
|
+
const FIREBENDER_CONFIG_FILE_NAME = "firebender.json";
|
|
20453
|
+
/** `.firebender/AGENTS.md`, the first file Firebender looks for in both scopes. */
|
|
20454
|
+
const FIREBENDER_AGENTS_MD_FILE_NAME = "AGENTS.md";
|
|
20455
|
+
(0, node_path.join)(FIREBENDER_DIR, "rules");
|
|
20456
|
+
/** Commands: `.firebender/commands/*.mdc` (project) and `~/.firebender/commands/*.mdc` (global). */
|
|
20457
|
+
const FIREBENDER_COMMANDS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, "commands");
|
|
20458
|
+
/** Agents: Markdown files registered through the `agents` array of `firebender.json`. */
|
|
20459
|
+
const FIREBENDER_AGENTS_DIR_NAME = "agents";
|
|
20460
|
+
const FIREBENDER_AGENTS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, FIREBENDER_AGENTS_DIR_NAME);
|
|
20461
|
+
/** Skills: `.firebender/skills/<name>/SKILL.md` (project) and `~/.firebender/skills/` (global). */
|
|
20462
|
+
const FIREBENDER_SKILLS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, "skills");
|
|
20463
|
+
/** Hooks: `.firebender/hooks.json` (project) and `~/.firebender/hooks.json` (global). */
|
|
20464
|
+
const FIREBENDER_HOOKS_FILE_NAME = "hooks.json";
|
|
20465
|
+
/**
|
|
20466
|
+
* The directory holding `firebender.json` for a scope: the project root, or
|
|
20467
|
+
* `~/.firebender/` for the personal config.
|
|
20468
|
+
*/
|
|
20469
|
+
function getFirebenderConfigDir({ global }) {
|
|
20470
|
+
return global ? FIREBENDER_DIR : ".";
|
|
20471
|
+
}
|
|
20472
|
+
//#endregion
|
|
20473
|
+
//#region src/features/commands/firebender-command.ts
|
|
20474
|
+
/**
|
|
20475
|
+
* Frontmatter of a Firebender command. Every field is optional: `name`
|
|
20476
|
+
* defaults to the file name, `model` is `default`, `quick` or a model ID, and
|
|
20477
|
+
* `mode` is one of `auto`, `read`, `write` or `composer`. Unknown keys are
|
|
20478
|
+
* preserved.
|
|
20479
|
+
*
|
|
20480
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20481
|
+
*/
|
|
20482
|
+
const FirebenderCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
20483
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
20484
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
20485
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
20486
|
+
mode: zod_mini.z.optional(zod_mini.z.string())
|
|
20487
|
+
});
|
|
20488
|
+
const FIREBENDER_COMMAND_EXTENSION = ".mdc";
|
|
20489
|
+
/**
|
|
20490
|
+
* Command generator for Firebender: one `.mdc` file per command under
|
|
20491
|
+
* `.firebender/commands/` (project) and `~/.firebender/commands/` (global),
|
|
20492
|
+
* with optional `name` / `description` / `model` / `mode` frontmatter. The
|
|
20493
|
+
* canonical `description` is written as is; `name`, `model` and `mode` come
|
|
20494
|
+
* from the `firebender:` section of the rulesync command.
|
|
20495
|
+
*
|
|
20496
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20497
|
+
*/
|
|
20498
|
+
var FirebenderCommand = class FirebenderCommand extends ToolCommand {
|
|
20499
|
+
frontmatter;
|
|
20500
|
+
body;
|
|
20501
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
20502
|
+
if (rest.validate) {
|
|
20503
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
|
|
20504
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
20505
|
+
}
|
|
20506
|
+
super({
|
|
20507
|
+
...rest,
|
|
20508
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
|
|
20509
|
+
});
|
|
20510
|
+
this.frontmatter = frontmatter;
|
|
20511
|
+
this.body = body;
|
|
20512
|
+
}
|
|
20513
|
+
static getSettablePaths(_options = {}) {
|
|
20514
|
+
return { relativeDirPath: FIREBENDER_COMMANDS_DIR_PATH };
|
|
20515
|
+
}
|
|
20516
|
+
getBody() {
|
|
20517
|
+
return this.body;
|
|
20518
|
+
}
|
|
20519
|
+
getFrontmatter() {
|
|
20520
|
+
return this.frontmatter;
|
|
20521
|
+
}
|
|
20522
|
+
toRulesyncCommand() {
|
|
20523
|
+
const { description, ...restFields } = this.frontmatter;
|
|
20524
|
+
const rulesyncFrontmatter = {
|
|
20525
|
+
targets: ["*"],
|
|
20526
|
+
description,
|
|
20527
|
+
...Object.keys(restFields).length > 0 && { firebender: restFields }
|
|
20528
|
+
};
|
|
20529
|
+
return new RulesyncCommand({
|
|
20530
|
+
outputRoot: ".",
|
|
20531
|
+
frontmatter: rulesyncFrontmatter,
|
|
20532
|
+
body: this.body,
|
|
20533
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
20534
|
+
relativeFilePath: this.relativeFilePath.replace(/\.mdc$/, ".md"),
|
|
20535
|
+
fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
|
|
20536
|
+
validate: true
|
|
20537
|
+
});
|
|
20538
|
+
}
|
|
20539
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
20540
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
20541
|
+
const firebenderFields = rulesyncFrontmatter.firebender ?? {};
|
|
20542
|
+
const frontmatter = {
|
|
20543
|
+
...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
|
|
20544
|
+
...firebenderFields
|
|
20545
|
+
};
|
|
20546
|
+
return new FirebenderCommand({
|
|
20547
|
+
outputRoot,
|
|
20548
|
+
frontmatter,
|
|
20549
|
+
body: rulesyncCommand.getBody(),
|
|
20550
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
20551
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(/\.md$/, FIREBENDER_COMMAND_EXTENSION),
|
|
20552
|
+
validate
|
|
20553
|
+
});
|
|
20554
|
+
}
|
|
20555
|
+
validate() {
|
|
20556
|
+
if (!this.frontmatter) return {
|
|
20557
|
+
success: true,
|
|
20558
|
+
error: null
|
|
20559
|
+
};
|
|
20560
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(this.frontmatter);
|
|
20561
|
+
if (result.success) return {
|
|
20562
|
+
success: true,
|
|
20563
|
+
error: null
|
|
20564
|
+
};
|
|
20565
|
+
return {
|
|
20566
|
+
success: false,
|
|
20567
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
20568
|
+
};
|
|
20569
|
+
}
|
|
20570
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
20571
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
20572
|
+
rulesyncCommand,
|
|
20573
|
+
toolTarget: "firebender"
|
|
20574
|
+
});
|
|
20575
|
+
}
|
|
20576
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
20577
|
+
const paths = this.getSettablePaths({ global });
|
|
20578
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
20579
|
+
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
20580
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
|
|
20581
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
20582
|
+
return new FirebenderCommand({
|
|
20583
|
+
outputRoot,
|
|
20584
|
+
relativeDirPath: paths.relativeDirPath,
|
|
20585
|
+
relativeFilePath,
|
|
20586
|
+
frontmatter: result.data,
|
|
20587
|
+
body: body.trim(),
|
|
20588
|
+
validate
|
|
20589
|
+
});
|
|
20590
|
+
}
|
|
20591
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
20592
|
+
return new FirebenderCommand({
|
|
20593
|
+
outputRoot,
|
|
20594
|
+
relativeDirPath,
|
|
20595
|
+
relativeFilePath,
|
|
20596
|
+
frontmatter: {},
|
|
20597
|
+
body: "",
|
|
20598
|
+
validate: false
|
|
20599
|
+
});
|
|
20600
|
+
}
|
|
20601
|
+
};
|
|
20602
|
+
//#endregion
|
|
20254
20603
|
//#region src/features/commands/gitlabduo-command.ts
|
|
20255
20604
|
/**
|
|
20256
20605
|
* GitLab Duo CLI custom slash commands are Markdown files named after the
|
|
@@ -20843,6 +21192,60 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
|
|
|
20843
21192
|
}
|
|
20844
21193
|
};
|
|
20845
21194
|
//#endregion
|
|
21195
|
+
//#region src/features/shared/hermes-plugin-commands.ts
|
|
21196
|
+
/**
|
|
21197
|
+
* Normalize a name into a Hermes Agent plugin slash command name.
|
|
21198
|
+
*
|
|
21199
|
+
* `ctx.register_command()` lowercases the name, and Hermes' messaging gateway
|
|
21200
|
+
* looks plugin commands up with every `_` turned into `-`
|
|
21201
|
+
* (`gateway/run_inbound_hooks.py`), so only a lowercase, hyphenated name is
|
|
21202
|
+
* reachable from every surface.
|
|
21203
|
+
*/
|
|
21204
|
+
function hermesSlashName(value) {
|
|
21205
|
+
return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
21206
|
+
}
|
|
21207
|
+
/**
|
|
21208
|
+
* Python helpers shared by the generated Hermes command plugins.
|
|
21209
|
+
*
|
|
21210
|
+
* A plugin slash command runs outside any agent turn, so it cannot call
|
|
21211
|
+
* `delegate_task` (which requires a parent agent). Instead the handler queues
|
|
21212
|
+
* its prompt as a user message with `ctx.inject_message()`, passing the calling
|
|
21213
|
+
* session's key: a `session_key` handler keyword on newer Hermes, or
|
|
21214
|
+
* `HERMES_SESSION_KEY` from the session context the gateway binds around the
|
|
21215
|
+
* handler on older releases.
|
|
21216
|
+
*/
|
|
21217
|
+
function getHermesQueueUserMessagePython({ pluginName }) {
|
|
21218
|
+
return `def _session_key(kwargs):
|
|
21219
|
+
session_key = kwargs.get("session_key")
|
|
21220
|
+
if session_key:
|
|
21221
|
+
return session_key
|
|
21222
|
+
try:
|
|
21223
|
+
from gateway.session_context import get_session_env
|
|
21224
|
+
except Exception:
|
|
21225
|
+
return None
|
|
21226
|
+
return get_session_env("HERMES_SESSION_KEY") or None
|
|
21227
|
+
|
|
21228
|
+
|
|
21229
|
+
def _queue_user_message(ctx, message, kwargs, label):
|
|
21230
|
+
"""Queue *message* as the session's next user turn; return an error reply, or None."""
|
|
21231
|
+
session_key = _session_key(kwargs)
|
|
21232
|
+
try:
|
|
21233
|
+
if session_key:
|
|
21234
|
+
queued = ctx.inject_message(message, role="user", session_key=session_key)
|
|
21235
|
+
else:
|
|
21236
|
+
queued = ctx.inject_message(message, role="user")
|
|
21237
|
+
except Exception as error:
|
|
21238
|
+
return f"Could not queue {label}: {error}"
|
|
21239
|
+
if not queued:
|
|
21240
|
+
return (
|
|
21241
|
+
f"Hermes refused to queue {label}: there is no active session, or this plugin "
|
|
21242
|
+
"may not inject messages. In gateway sessions, set "
|
|
21243
|
+
"plugins.entries.${pluginName}.allow_gateway_injection: true in config.yaml."
|
|
21244
|
+
)
|
|
21245
|
+
return None
|
|
21246
|
+
`;
|
|
21247
|
+
}
|
|
21248
|
+
//#endregion
|
|
20846
21249
|
//#region src/features/skills/agentsskills-skill.ts
|
|
20847
21250
|
const AgentsSkillsSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
20848
21251
|
name: zod_mini.z.string(),
|
|
@@ -21325,9 +21728,6 @@ const HermesCommandSpecSchema = zod_mini.z.object({
|
|
|
21325
21728
|
description: zod_mini.z.string(),
|
|
21326
21729
|
prompt: zod_mini.z.string()
|
|
21327
21730
|
});
|
|
21328
|
-
function hermesSlashName(value) {
|
|
21329
|
-
return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
21330
|
-
}
|
|
21331
21731
|
function getPluginManifestContent$2() {
|
|
21332
21732
|
return [
|
|
21333
21733
|
"name: rulesync-commands",
|
|
@@ -21361,6 +21761,8 @@ def _load_commands():
|
|
|
21361
21761
|
return commands
|
|
21362
21762
|
|
|
21363
21763
|
|
|
21764
|
+
${getHermesQueueUserMessagePython({ pluginName: "rulesync-commands" })}
|
|
21765
|
+
|
|
21364
21766
|
def _register_command(ctx, command):
|
|
21365
21767
|
slug = command.get("slug")
|
|
21366
21768
|
if not slug:
|
|
@@ -21370,31 +21772,22 @@ def _register_command(ctx, command):
|
|
|
21370
21772
|
prompt = command.get("prompt") or ""
|
|
21371
21773
|
|
|
21372
21774
|
def handler(args=None, **kwargs):
|
|
21373
|
-
del kwargs
|
|
21374
21775
|
raw_args = ""
|
|
21375
21776
|
if isinstance(args, dict):
|
|
21376
21777
|
raw_args = args.get("args") or args.get("context") or args.get("prompt") or ""
|
|
21377
21778
|
elif args is not None:
|
|
21378
21779
|
raw_args = str(args)
|
|
21379
21780
|
|
|
21380
|
-
|
|
21781
|
+
parts = [prompt] if prompt else []
|
|
21381
21782
|
if raw_args:
|
|
21382
|
-
|
|
21783
|
+
parts.append(f"User arguments:\\n{raw_args}")
|
|
21383
21784
|
|
|
21384
|
-
#
|
|
21385
|
-
#
|
|
21386
|
-
#
|
|
21387
|
-
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
"tasks": [
|
|
21391
|
-
{
|
|
21392
|
-
"goal": description,
|
|
21393
|
-
"context": "\\n\\n".join(context_parts),
|
|
21394
|
-
},
|
|
21395
|
-
],
|
|
21396
|
-
},
|
|
21397
|
-
)
|
|
21785
|
+
# A slash command runs outside any agent turn, so calling delegate_task
|
|
21786
|
+
# here fails with "delegate_task requires a parent agent context".
|
|
21787
|
+
# Queue the command prompt as the session's next user message instead.
|
|
21788
|
+
label = f"the /{slug} command"
|
|
21789
|
+
error = _queue_user_message(ctx, "\\n\\n".join(parts) or description, kwargs, label)
|
|
21790
|
+
return error or f"Queued {label}."
|
|
21398
21791
|
|
|
21399
21792
|
ctx.register_command(slug, handler, description)
|
|
21400
21793
|
|
|
@@ -25266,6 +25659,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
25266
25659
|
supportsSubdirectory: false
|
|
25267
25660
|
}
|
|
25268
25661
|
}],
|
|
25662
|
+
["firebender", {
|
|
25663
|
+
class: FirebenderCommand,
|
|
25664
|
+
meta: {
|
|
25665
|
+
extension: "mdc",
|
|
25666
|
+
supportsProject: true,
|
|
25667
|
+
supportsGlobal: true,
|
|
25668
|
+
isSimulated: false,
|
|
25669
|
+
supportsSubdirectory: false
|
|
25670
|
+
}
|
|
25671
|
+
}],
|
|
25269
25672
|
["goose", {
|
|
25270
25673
|
class: GooseCommand,
|
|
25271
25674
|
meta: {
|
|
@@ -32549,6 +32952,116 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
32549
32952
|
}
|
|
32550
32953
|
};
|
|
32551
32954
|
//#endregion
|
|
32955
|
+
//#region src/features/hooks/firebender-hooks.ts
|
|
32956
|
+
/**
|
|
32957
|
+
* Hooks generator for Firebender.
|
|
32958
|
+
*
|
|
32959
|
+
* Firebender reads `.firebender/hooks.json` (project) and
|
|
32960
|
+
* `~/.firebender/hooks.json` (global). The file follows Cursor's layout — a
|
|
32961
|
+
* `version` plus a `hooks` map from camelCase event names to lists of
|
|
32962
|
+
* `{ command, timeout?, loop_limit?, matcher? }` entries — and every event is
|
|
32963
|
+
* spelled like its canonical counterpart. The per-hook `type` accepts
|
|
32964
|
+
* `"command" | "prompt"`, but the docs never say what a prompt hook does, so
|
|
32965
|
+
* only command hooks are generated (the HooksProcessor warns about the rest).
|
|
32966
|
+
*
|
|
32967
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
32968
|
+
*/
|
|
32969
|
+
var FirebenderHooks = class FirebenderHooks extends ToolHooks {
|
|
32970
|
+
constructor(params) {
|
|
32971
|
+
super({
|
|
32972
|
+
...params,
|
|
32973
|
+
fileContent: params.fileContent ?? "{}"
|
|
32974
|
+
});
|
|
32975
|
+
}
|
|
32976
|
+
static getSettablePaths(_options = {}) {
|
|
32977
|
+
return {
|
|
32978
|
+
relativeDirPath: FIREBENDER_DIR,
|
|
32979
|
+
relativeFilePath: FIREBENDER_HOOKS_FILE_NAME
|
|
32980
|
+
};
|
|
32981
|
+
}
|
|
32982
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32983
|
+
const paths = FirebenderHooks.getSettablePaths({ global });
|
|
32984
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
32985
|
+
return new FirebenderHooks({
|
|
32986
|
+
outputRoot,
|
|
32987
|
+
relativeDirPath: paths.relativeDirPath,
|
|
32988
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32989
|
+
fileContent,
|
|
32990
|
+
validate
|
|
32991
|
+
});
|
|
32992
|
+
}
|
|
32993
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
32994
|
+
const config = rulesyncHooks.getJson();
|
|
32995
|
+
const supportedEvents = new Set(FIREBENDER_HOOK_EVENTS);
|
|
32996
|
+
const sharedHooks = {};
|
|
32997
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (supportedEvents.has(event)) sharedHooks[event] = defs;
|
|
32998
|
+
const mergedHooks = {
|
|
32999
|
+
...sharedHooks,
|
|
33000
|
+
...config.firebender?.hooks
|
|
33001
|
+
};
|
|
33002
|
+
const firebenderHooks = {};
|
|
33003
|
+
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
33004
|
+
const mappedDefs = defs.filter((def) => (def.type ?? "command") === "command").filter((def) => typeof def.command === "string" && def.command.length > 0).map((def) => ({
|
|
33005
|
+
command: def.command,
|
|
33006
|
+
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
33007
|
+
...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
|
|
33008
|
+
...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher }
|
|
33009
|
+
}));
|
|
33010
|
+
if (mappedDefs.length > 0) firebenderHooks[eventName] = mappedDefs;
|
|
33011
|
+
}
|
|
33012
|
+
const paths = FirebenderHooks.getSettablePaths({ global });
|
|
33013
|
+
const merged = mergeGeneratedHookLists({
|
|
33014
|
+
existingContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
|
|
33015
|
+
generatedHooks: firebenderHooks,
|
|
33016
|
+
shape: "flat",
|
|
33017
|
+
preserveUnowned,
|
|
33018
|
+
previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
33019
|
+
logger
|
|
33020
|
+
});
|
|
33021
|
+
const fileContent = JSON.stringify({
|
|
33022
|
+
version: config.version ?? 1,
|
|
33023
|
+
hooks: merged.hooks
|
|
33024
|
+
}, null, 2);
|
|
33025
|
+
return new FirebenderHooks({
|
|
33026
|
+
outputRoot,
|
|
33027
|
+
relativeDirPath: paths.relativeDirPath,
|
|
33028
|
+
relativeFilePath: paths.relativeFilePath,
|
|
33029
|
+
fileContent,
|
|
33030
|
+
ownedHookRefs: preserveUnowned ? merged.owned : void 0,
|
|
33031
|
+
validate
|
|
33032
|
+
});
|
|
33033
|
+
}
|
|
33034
|
+
static supportsPreserveUnowned() {
|
|
33035
|
+
return true;
|
|
33036
|
+
}
|
|
33037
|
+
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
33038
|
+
return toolHooks instanceof FirebenderHooks ? toolHooks.getOwnershipLockFiles() : [];
|
|
33039
|
+
}
|
|
33040
|
+
toRulesyncHooks() {
|
|
33041
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
33042
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
33043
|
+
hooks: parsed.hooks ?? {},
|
|
33044
|
+
overrideKey: "firebender",
|
|
33045
|
+
version: parsed.version ?? 1
|
|
33046
|
+
}), null, 2) });
|
|
33047
|
+
}
|
|
33048
|
+
validate() {
|
|
33049
|
+
return {
|
|
33050
|
+
success: true,
|
|
33051
|
+
error: null
|
|
33052
|
+
};
|
|
33053
|
+
}
|
|
33054
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
33055
|
+
return new FirebenderHooks({
|
|
33056
|
+
outputRoot,
|
|
33057
|
+
relativeDirPath,
|
|
33058
|
+
relativeFilePath,
|
|
33059
|
+
fileContent: "{}",
|
|
33060
|
+
validate: false
|
|
33061
|
+
});
|
|
33062
|
+
}
|
|
33063
|
+
};
|
|
33064
|
+
//#endregion
|
|
32552
33065
|
//#region src/features/hooks/gitlabduo-hooks.ts
|
|
32553
33066
|
const GITLABDUO_CONVERTER_CONFIG = {
|
|
32554
33067
|
supportedEvents: GITLABDUO_HOOK_EVENTS,
|
|
@@ -36959,6 +37472,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
36959
37472
|
supportedHookTypes: ["command", "prompt"],
|
|
36960
37473
|
supportsMatcher: true
|
|
36961
37474
|
}],
|
|
37475
|
+
["firebender", {
|
|
37476
|
+
class: FirebenderHooks,
|
|
37477
|
+
meta: {
|
|
37478
|
+
supportsProject: true,
|
|
37479
|
+
supportsGlobal: true,
|
|
37480
|
+
supportsImport: true
|
|
37481
|
+
},
|
|
37482
|
+
supportedEvents: FIREBENDER_HOOK_EVENTS,
|
|
37483
|
+
supportedHookTypes: ["command"],
|
|
37484
|
+
supportsMatcher: true
|
|
37485
|
+
}],
|
|
36962
37486
|
["claudecode", {
|
|
36963
37487
|
class: ClaudecodeHooks,
|
|
36964
37488
|
meta: {
|
|
@@ -37830,6 +38354,67 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
|
|
|
37830
38354
|
}
|
|
37831
38355
|
};
|
|
37832
38356
|
//#endregion
|
|
38357
|
+
//#region src/constants/androidstudio-paths.ts
|
|
38358
|
+
/**
|
|
38359
|
+
* Gemini in Android Studio configuration-layout conventions.
|
|
38360
|
+
*
|
|
38361
|
+
* Only the Android Studio-specific files are owned by the `androidstudio`
|
|
38362
|
+
* target. The shared `AGENTS.md` and `.agents/skills/` surfaces Gemini also
|
|
38363
|
+
* reads belong to the `agentsmd` and `agentsskills` targets.
|
|
38364
|
+
*/
|
|
38365
|
+
const ANDROIDSTUDIO_IGNORE_FILE_NAME = ".aiexclude";
|
|
38366
|
+
const ANDROIDSTUDIO_SKILLS_DIR_PATH = (0, node_path.join)(".android-studio", "skills");
|
|
38367
|
+
//#endregion
|
|
38368
|
+
//#region src/features/ignore/androidstudio-ignore.ts
|
|
38369
|
+
/**
|
|
38370
|
+
* Ignore generator for Gemini in Android Studio.
|
|
38371
|
+
*
|
|
38372
|
+
* Gemini reads `.aiexclude` files (gitignore syntax) anywhere in the project;
|
|
38373
|
+
* each one applies to the files at or below its directory and blocks both the
|
|
38374
|
+
* chat and the editor AI features such as code completion. Rulesync writes the
|
|
38375
|
+
* project-root file only. There is no user-level variant.
|
|
38376
|
+
*
|
|
38377
|
+
* @see https://developer.android.com/studio/gemini/aiexclude
|
|
38378
|
+
*/
|
|
38379
|
+
var AndroidstudioIgnore = class AndroidstudioIgnore extends ToolIgnore {
|
|
38380
|
+
static getSettablePaths() {
|
|
38381
|
+
return {
|
|
38382
|
+
relativeDirPath: ".",
|
|
38383
|
+
relativeFilePath: ANDROIDSTUDIO_IGNORE_FILE_NAME
|
|
38384
|
+
};
|
|
38385
|
+
}
|
|
38386
|
+
toRulesyncIgnore() {
|
|
38387
|
+
return this.toRulesyncIgnoreDefault();
|
|
38388
|
+
}
|
|
38389
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
38390
|
+
return new AndroidstudioIgnore({
|
|
38391
|
+
outputRoot,
|
|
38392
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
38393
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
38394
|
+
fileContent: rulesyncIgnore.getFileContent()
|
|
38395
|
+
});
|
|
38396
|
+
}
|
|
38397
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
38398
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
38399
|
+
return new AndroidstudioIgnore({
|
|
38400
|
+
outputRoot,
|
|
38401
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
38402
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
38403
|
+
fileContent,
|
|
38404
|
+
validate
|
|
38405
|
+
});
|
|
38406
|
+
}
|
|
38407
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
38408
|
+
return new AndroidstudioIgnore({
|
|
38409
|
+
outputRoot,
|
|
38410
|
+
relativeDirPath,
|
|
38411
|
+
relativeFilePath,
|
|
38412
|
+
fileContent: "",
|
|
38413
|
+
validate: false
|
|
38414
|
+
});
|
|
38415
|
+
}
|
|
38416
|
+
};
|
|
38417
|
+
//#endregion
|
|
37833
38418
|
//#region src/features/ignore/antigravity-cli-ignore.ts
|
|
37834
38419
|
/**
|
|
37835
38420
|
* Antigravity CLI is the successor to Gemini CLI and is built on the same
|
|
@@ -38491,6 +39076,95 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
38491
39076
|
}
|
|
38492
39077
|
};
|
|
38493
39078
|
//#endregion
|
|
39079
|
+
//#region src/features/ignore/firebender-ignore.ts
|
|
39080
|
+
function readIgnorePatterns(fileContent) {
|
|
39081
|
+
const ignore = parseSharedConfig({
|
|
39082
|
+
format: "json",
|
|
39083
|
+
fileContent
|
|
39084
|
+
}).ignore;
|
|
39085
|
+
if (!Array.isArray(ignore)) return [];
|
|
39086
|
+
return ignore.filter((pattern) => typeof pattern === "string");
|
|
39087
|
+
}
|
|
39088
|
+
/**
|
|
39089
|
+
* Ignore generator for Firebender.
|
|
39090
|
+
*
|
|
39091
|
+
* Firebender reads gitignore-style patterns from the `ignore` array of
|
|
39092
|
+
* `firebender.json` (project root) and `~/.firebender/firebender.json`
|
|
39093
|
+
* (personal); the two lists are appended together. The file is shared with the
|
|
39094
|
+
* MCP (`mcpServers`) and subagents (`agents`) features and carries settings
|
|
39095
|
+
* rulesync does not own, so only the `ignore` key is written and the file is
|
|
39096
|
+
* never deleted.
|
|
39097
|
+
*
|
|
39098
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
39099
|
+
*/
|
|
39100
|
+
var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
39101
|
+
constructor(params) {
|
|
39102
|
+
super(params);
|
|
39103
|
+
this.patterns = readIgnorePatterns(this.fileContent || "{}");
|
|
39104
|
+
}
|
|
39105
|
+
static getSettablePaths({ global = false } = {}) {
|
|
39106
|
+
return {
|
|
39107
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
39108
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
39109
|
+
};
|
|
39110
|
+
}
|
|
39111
|
+
/** `firebender.json` is a user-managed config file, so rulesync never deletes it. */
|
|
39112
|
+
isDeletable() {
|
|
39113
|
+
return false;
|
|
39114
|
+
}
|
|
39115
|
+
toRulesyncIgnore() {
|
|
39116
|
+
return new RulesyncIgnore({
|
|
39117
|
+
outputRoot: ".",
|
|
39118
|
+
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
39119
|
+
relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
|
|
39120
|
+
fileContent: this.patterns.filter((pattern) => pattern.length > 0).join("\n")
|
|
39121
|
+
});
|
|
39122
|
+
}
|
|
39123
|
+
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
39124
|
+
const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
39125
|
+
const paths = this.getSettablePaths({ global });
|
|
39126
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
39127
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
39128
|
+
const managedPatterns = [...new Set(patterns)];
|
|
39129
|
+
return new FirebenderIgnore({
|
|
39130
|
+
outputRoot,
|
|
39131
|
+
relativeDirPath: paths.relativeDirPath,
|
|
39132
|
+
relativeFilePath: paths.relativeFilePath,
|
|
39133
|
+
fileContent: applySharedConfigPatch({
|
|
39134
|
+
fileKey: sharedConfigFileKey(paths),
|
|
39135
|
+
feature: "ignore",
|
|
39136
|
+
existingContent,
|
|
39137
|
+
patch: { ignore: managedPatterns.length > 0 ? managedPatterns : void 0 },
|
|
39138
|
+
filePath
|
|
39139
|
+
}),
|
|
39140
|
+
validate: true,
|
|
39141
|
+
global
|
|
39142
|
+
});
|
|
39143
|
+
}
|
|
39144
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
39145
|
+
const paths = this.getSettablePaths({ global });
|
|
39146
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
39147
|
+
return new FirebenderIgnore({
|
|
39148
|
+
outputRoot,
|
|
39149
|
+
relativeDirPath: paths.relativeDirPath,
|
|
39150
|
+
relativeFilePath: paths.relativeFilePath,
|
|
39151
|
+
fileContent,
|
|
39152
|
+
validate,
|
|
39153
|
+
global
|
|
39154
|
+
});
|
|
39155
|
+
}
|
|
39156
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
39157
|
+
return new FirebenderIgnore({
|
|
39158
|
+
outputRoot,
|
|
39159
|
+
relativeDirPath,
|
|
39160
|
+
relativeFilePath,
|
|
39161
|
+
fileContent: "{}",
|
|
39162
|
+
validate: false,
|
|
39163
|
+
global
|
|
39164
|
+
});
|
|
39165
|
+
}
|
|
39166
|
+
};
|
|
39167
|
+
//#endregion
|
|
38494
39168
|
//#region src/features/ignore/grokcli-ignore.ts
|
|
38495
39169
|
/** The base built-in profile the rulesync profile extends (Grok's own default). */
|
|
38496
39170
|
const GROKCLI_SANDBOX_BASE_PROFILE = "workspace";
|
|
@@ -39808,6 +40482,7 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
|
|
|
39808
40482
|
const IgnoreProcessorToolTargetSchema = zod_mini.z.enum(ignoreProcessorToolTargetTuple);
|
|
39809
40483
|
const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
39810
40484
|
["aiassistant", { class: AiassistantIgnore }],
|
|
40485
|
+
["androidstudio", { class: AndroidstudioIgnore }],
|
|
39811
40486
|
["antigravity-cli", { class: AntigravityCliIgnore }],
|
|
39812
40487
|
["augmentcode", { class: AugmentcodeIgnore }],
|
|
39813
40488
|
["bob", { class: BobIgnore }],
|
|
@@ -39818,6 +40493,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
39818
40493
|
["continue", { class: ContinueIgnore }],
|
|
39819
40494
|
["crush", { class: CrushIgnore }],
|
|
39820
40495
|
["cursor", { class: CursorIgnore }],
|
|
40496
|
+
["firebender", { class: FirebenderIgnore }],
|
|
39821
40497
|
["grokcli", { class: GrokcliIgnore }],
|
|
39822
40498
|
["hermesagent", { class: HermesagentIgnore }],
|
|
39823
40499
|
["junie", { class: JunieIgnore }],
|
|
@@ -39842,6 +40518,7 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
|
|
|
39842
40518
|
const ignoreProcessorGlobalToolTargets = [
|
|
39843
40519
|
"continue",
|
|
39844
40520
|
"devin",
|
|
40521
|
+
"firebender",
|
|
39845
40522
|
"grokcli",
|
|
39846
40523
|
"kilo",
|
|
39847
40524
|
"kiro",
|
|
@@ -45339,6 +46016,171 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
45339
46016
|
}
|
|
45340
46017
|
};
|
|
45341
46018
|
//#endregion
|
|
46019
|
+
//#region src/features/mcp/firebender-mcp.ts
|
|
46020
|
+
/**
|
|
46021
|
+
* The keys of the canonical server schema. Firebender reads only `command`,
|
|
46022
|
+
* `args`, `env`, `url` and `headers` of them, so the rest are dropped; a key
|
|
46023
|
+
* NOT on this list is unknown to rulesync and passed through untouched, so a
|
|
46024
|
+
* Firebender-native field such as `deferLoading` written in the tool-scoped
|
|
46025
|
+
* `firebender.mcpServers` block (or on a shared server) survives.
|
|
46026
|
+
*/
|
|
46027
|
+
const CANONICAL_MCP_SERVER_KEYS$1 = new Set(Object.keys(McpServerSchema.def.shape));
|
|
46028
|
+
function passthroughNonCanonicalFields$1(serverConfig) {
|
|
46029
|
+
return Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !CANONICAL_MCP_SERVER_KEYS$1.has(key)));
|
|
46030
|
+
}
|
|
46031
|
+
/**
|
|
46032
|
+
* Translate one canonical server into Firebender's `mcpServers` entry, or
|
|
46033
|
+
* return `null` (with a warning) for a server Firebender cannot express. A
|
|
46034
|
+
* stdio server is `{ command, args?, env? }`, a remote one `{ url, headers? }`;
|
|
46035
|
+
* Firebender has no `type` field and no per-server disable switch.
|
|
46036
|
+
*
|
|
46037
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
46038
|
+
*/
|
|
46039
|
+
function convertServerToFirebenderFormat({ serverName, serverConfig, logger }) {
|
|
46040
|
+
if (serverConfig.disabled === true) return warnAndSkipMcpServer({
|
|
46041
|
+
toolName: "Firebender",
|
|
46042
|
+
serverName,
|
|
46043
|
+
reason: "`disabled: true`, and Firebender has no per-server disable switch",
|
|
46044
|
+
logger
|
|
46045
|
+
});
|
|
46046
|
+
if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
|
|
46047
|
+
toolName: "Firebender",
|
|
46048
|
+
serverName,
|
|
46049
|
+
reason: "no command or url",
|
|
46050
|
+
logger
|
|
46051
|
+
});
|
|
46052
|
+
const passthrough = passthroughNonCanonicalFields$1(serverConfig);
|
|
46053
|
+
if (isRemoteMcpServer(serverConfig)) {
|
|
46054
|
+
const url = resolveRemoteMcpUrl(serverConfig);
|
|
46055
|
+
if (url === void 0) return warnAndSkipMcpServer({
|
|
46056
|
+
toolName: "Firebender",
|
|
46057
|
+
serverName,
|
|
46058
|
+
reason: "a remote transport but no url",
|
|
46059
|
+
logger
|
|
46060
|
+
});
|
|
46061
|
+
return {
|
|
46062
|
+
...passthrough,
|
|
46063
|
+
url,
|
|
46064
|
+
...serverConfig.headers !== void 0 && { headers: serverConfig.headers }
|
|
46065
|
+
};
|
|
46066
|
+
}
|
|
46067
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
46068
|
+
if (command === void 0) return warnAndSkipMcpServer({
|
|
46069
|
+
toolName: "Firebender",
|
|
46070
|
+
serverName,
|
|
46071
|
+
reason: "a local transport but no command",
|
|
46072
|
+
logger
|
|
46073
|
+
});
|
|
46074
|
+
return {
|
|
46075
|
+
...passthrough,
|
|
46076
|
+
command,
|
|
46077
|
+
...args.length > 0 && { args },
|
|
46078
|
+
...serverConfig.env !== void 0 && { env: serverConfig.env }
|
|
46079
|
+
};
|
|
46080
|
+
}
|
|
46081
|
+
/**
|
|
46082
|
+
* MCP generator for Firebender.
|
|
46083
|
+
*
|
|
46084
|
+
* Firebender reads MCP servers from the `mcpServers` key of `firebender.json`
|
|
46085
|
+
* (project root) and `~/.firebender/firebender.json` (personal; it wins on
|
|
46086
|
+
* name clashes). The file is shared with the ignore (`ignore`) and subagents
|
|
46087
|
+
* (`agents`) features and carries settings rulesync does not own, so only the
|
|
46088
|
+
* `mcpServers` key is written and the file is never deleted.
|
|
46089
|
+
*
|
|
46090
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
46091
|
+
*/
|
|
46092
|
+
var FirebenderMcp = class FirebenderMcp extends ToolMcp {
|
|
46093
|
+
json;
|
|
46094
|
+
constructor(params) {
|
|
46095
|
+
super(params);
|
|
46096
|
+
this.json = parseSharedConfig({
|
|
46097
|
+
format: "json",
|
|
46098
|
+
fileContent: this.fileContent || "{}"
|
|
46099
|
+
});
|
|
46100
|
+
}
|
|
46101
|
+
getJson() {
|
|
46102
|
+
return this.json;
|
|
46103
|
+
}
|
|
46104
|
+
static getSettablePaths({ global = false } = {}) {
|
|
46105
|
+
return {
|
|
46106
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
46107
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
46108
|
+
};
|
|
46109
|
+
}
|
|
46110
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
46111
|
+
const paths = this.getSettablePaths({ global });
|
|
46112
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
46113
|
+
return new FirebenderMcp({
|
|
46114
|
+
outputRoot,
|
|
46115
|
+
relativeDirPath: paths.relativeDirPath,
|
|
46116
|
+
relativeFilePath: paths.relativeFilePath,
|
|
46117
|
+
fileContent,
|
|
46118
|
+
validate,
|
|
46119
|
+
global
|
|
46120
|
+
});
|
|
46121
|
+
}
|
|
46122
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
46123
|
+
const paths = this.getSettablePaths({ global });
|
|
46124
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
46125
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
46126
|
+
const mcpServers = {};
|
|
46127
|
+
for (const [serverName, serverConfig] of Object.entries(rulesyncMcp.getMcpServers())) {
|
|
46128
|
+
const converted = convertServerToFirebenderFormat({
|
|
46129
|
+
serverName,
|
|
46130
|
+
serverConfig,
|
|
46131
|
+
logger
|
|
46132
|
+
});
|
|
46133
|
+
if (converted !== null) mcpServers[serverName] = converted;
|
|
46134
|
+
}
|
|
46135
|
+
return new FirebenderMcp({
|
|
46136
|
+
outputRoot,
|
|
46137
|
+
relativeDirPath: paths.relativeDirPath,
|
|
46138
|
+
relativeFilePath: paths.relativeFilePath,
|
|
46139
|
+
fileContent: applySharedConfigPatch({
|
|
46140
|
+
fileKey: sharedConfigFileKey(paths),
|
|
46141
|
+
feature: "mcp",
|
|
46142
|
+
existingContent,
|
|
46143
|
+
patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
|
|
46144
|
+
filePath
|
|
46145
|
+
}),
|
|
46146
|
+
validate,
|
|
46147
|
+
global
|
|
46148
|
+
});
|
|
46149
|
+
}
|
|
46150
|
+
toRulesyncMcp() {
|
|
46151
|
+
const servers = this.json.mcpServers;
|
|
46152
|
+
const mcpServers = {};
|
|
46153
|
+
if (isMcpServers(servers)) {
|
|
46154
|
+
for (const [serverName, server] of Object.entries(servers)) if (server !== null && typeof server === "object" && !Array.isArray(server)) mcpServers[serverName] = server;
|
|
46155
|
+
}
|
|
46156
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
46157
|
+
}
|
|
46158
|
+
validate() {
|
|
46159
|
+
return {
|
|
46160
|
+
success: true,
|
|
46161
|
+
error: null
|
|
46162
|
+
};
|
|
46163
|
+
}
|
|
46164
|
+
/**
|
|
46165
|
+
* `firebender.json` is a user-managed file shared with other features (the
|
|
46166
|
+
* ignore feature's `ignore`, the subagents feature's `agents`), so it must
|
|
46167
|
+
* not be deleted.
|
|
46168
|
+
*/
|
|
46169
|
+
isDeletable() {
|
|
46170
|
+
return false;
|
|
46171
|
+
}
|
|
46172
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
46173
|
+
return new FirebenderMcp({
|
|
46174
|
+
outputRoot,
|
|
46175
|
+
relativeDirPath,
|
|
46176
|
+
relativeFilePath,
|
|
46177
|
+
fileContent: "{}",
|
|
46178
|
+
validate: false,
|
|
46179
|
+
global
|
|
46180
|
+
});
|
|
46181
|
+
}
|
|
46182
|
+
};
|
|
46183
|
+
//#endregion
|
|
45342
46184
|
//#region src/features/mcp/gitlabduo-mcp.ts
|
|
45343
46185
|
const CANONICAL_TO_GITLABDUO_TRANSPORT = {
|
|
45344
46186
|
local: "stdio",
|
|
@@ -52030,6 +52872,248 @@ var WarpcliMcp = class extends WarpMcp {
|
|
|
52030
52872
|
}
|
|
52031
52873
|
};
|
|
52032
52874
|
//#endregion
|
|
52875
|
+
//#region src/constants/xum-paths.ts
|
|
52876
|
+
/**
|
|
52877
|
+
* Xum configuration-layout conventions.
|
|
52878
|
+
*
|
|
52879
|
+
* Xum (Coder's desktop app and `xum run` CLI for running coding agents in
|
|
52880
|
+
* parallel workspaces, formerly Mux) keeps its project assets under a `.xum/`
|
|
52881
|
+
* directory at the project root and the matching user assets under `~/.xum/`.
|
|
52882
|
+
* Legacy `.mux/` files are still read when the matching `.xum/` file is
|
|
52883
|
+
* missing; rulesync writes only the `.xum/` paths.
|
|
52884
|
+
*
|
|
52885
|
+
* @see https://xum.cdr.dev/reference/mux-compatibility
|
|
52886
|
+
*/
|
|
52887
|
+
const XUM_DIR = ".xum";
|
|
52888
|
+
const XUM_RULE_FILE_NAME = "AGENTS.md";
|
|
52889
|
+
const XUM_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
|
|
52890
|
+
const XUM_MCP_FILE_NAME = "mcp.jsonc";
|
|
52891
|
+
const XUM_AGENTS_DIR_PATH = (0, node_path.join)(XUM_DIR, "agents");
|
|
52892
|
+
const XUM_SKILLS_DIR_PATH = (0, node_path.join)(XUM_DIR, "skills");
|
|
52893
|
+
//#endregion
|
|
52894
|
+
//#region src/features/mcp/xum-mcp.ts
|
|
52895
|
+
const EMPTY_XUM_MCP_CONFIG = JSON.stringify({ servers: {} }, null, 2);
|
|
52896
|
+
/** Characters that never need quoting in a POSIX shell word. */
|
|
52897
|
+
const SHELL_SAFE_WORD = /^[A-Za-z0-9_\-.,:/@%+=]+$/;
|
|
52898
|
+
/**
|
|
52899
|
+
* A command string free of quotes, escapes, expansions and operators, which
|
|
52900
|
+
* can be split on whitespace back into argv without changing its meaning.
|
|
52901
|
+
*/
|
|
52902
|
+
const SPLIT_SAFE_COMMAND = /^[^'"\\$`|&;<>(){}*?[\]~#!\n]+$/;
|
|
52903
|
+
/** Single-quote one argv element for the POSIX shell Xum runs commands in. */
|
|
52904
|
+
function quoteShellWord(value) {
|
|
52905
|
+
if (SHELL_SAFE_WORD.test(value)) return value;
|
|
52906
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
52907
|
+
}
|
|
52908
|
+
/**
|
|
52909
|
+
* Build the shell command string a Xum stdio server is launched with. A
|
|
52910
|
+
* `command` string is kept verbatim (Xum itself treats it as a raw shell
|
|
52911
|
+
* string, so `npx -y pkg` authored as one string keeps working); an explicit
|
|
52912
|
+
* argv (`command` as an array) and every `args` element are quoted where
|
|
52913
|
+
* needed, so `/opt/my tools/srv` stays one word.
|
|
52914
|
+
*/
|
|
52915
|
+
function buildCommandString(server) {
|
|
52916
|
+
const args = server.args ?? [];
|
|
52917
|
+
if (Array.isArray(server.command)) {
|
|
52918
|
+
const [executable] = server.command;
|
|
52919
|
+
if (executable === void 0 || executable.trim() === "") return void 0;
|
|
52920
|
+
return [...server.command, ...args].map(quoteShellWord).join(" ");
|
|
52921
|
+
}
|
|
52922
|
+
if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
|
|
52923
|
+
return [server.command, ...args.map(quoteShellWord)].join(" ");
|
|
52924
|
+
}
|
|
52925
|
+
/** Split a plain command string back into `command` + `args` when it is safe to. */
|
|
52926
|
+
function splitCommandString(command) {
|
|
52927
|
+
const trimmed = command.trim();
|
|
52928
|
+
if (!SPLIT_SAFE_COMMAND.test(trimmed)) return { command };
|
|
52929
|
+
const [head, ...rest] = trimmed.split(/\s+/);
|
|
52930
|
+
if (head === void 0) return { command };
|
|
52931
|
+
return rest.length > 0 ? {
|
|
52932
|
+
command: head,
|
|
52933
|
+
args: rest
|
|
52934
|
+
} : { command: head };
|
|
52935
|
+
}
|
|
52936
|
+
function resolveXumServerKind(server) {
|
|
52937
|
+
const candidate = server.type ?? server.transport;
|
|
52938
|
+
if (candidate === "stdio" || candidate === "local") return "stdio";
|
|
52939
|
+
if (candidate === "http" || candidate === "streamable-http") return "http";
|
|
52940
|
+
if (candidate === "sse") return "sse";
|
|
52941
|
+
if (candidate !== void 0) return { unsupported: candidate };
|
|
52942
|
+
if (server.command !== void 0) return "stdio";
|
|
52943
|
+
const url = server.url ?? server.httpUrl;
|
|
52944
|
+
if (typeof url === "string") return /^wss?:\/\//i.test(url) ? { unsupported: "ws" } : "auto";
|
|
52945
|
+
}
|
|
52946
|
+
/** Convert one canonical server into a Xum `servers` entry, or `null` to skip it. */
|
|
52947
|
+
function convertServerToXum({ name, server, logger }) {
|
|
52948
|
+
const skip = (reason) => {
|
|
52949
|
+
warnWithFallback(logger, `Xum MCP: skipping ${quoteValueForWarning(name)} because ${reason}.`);
|
|
52950
|
+
return null;
|
|
52951
|
+
};
|
|
52952
|
+
const kind = resolveXumServerKind(server);
|
|
52953
|
+
if (kind === void 0) return skip("it has neither a command nor a URL");
|
|
52954
|
+
if (typeof kind === "object") return skip(`it uses the "${kind.unsupported}" transport, which Xum does not support (only stdio, http and sse)`);
|
|
52955
|
+
const settings = {};
|
|
52956
|
+
if (server.disabled === true) settings.disabled = true;
|
|
52957
|
+
if (isStringArray$1(server.enabledTools)) settings.toolAllowlist = server.enabledTools;
|
|
52958
|
+
if (kind !== "stdio") {
|
|
52959
|
+
const url = server.url ?? server.httpUrl;
|
|
52960
|
+
if (typeof url !== "string" || url === "") return skip("it has no URL");
|
|
52961
|
+
return {
|
|
52962
|
+
...kind !== "auto" && { transport: kind },
|
|
52963
|
+
url,
|
|
52964
|
+
...isPlainObject$1(server.headers) && Object.keys(server.headers).length > 0 && { headers: server.headers },
|
|
52965
|
+
...settings
|
|
52966
|
+
};
|
|
52967
|
+
}
|
|
52968
|
+
const command = buildCommandString(server);
|
|
52969
|
+
if (command === void 0) return skip("it has no command");
|
|
52970
|
+
const dropped = [...isPlainObject$1(server.env) && Object.keys(server.env).length > 0 ? ["env"] : [], ...typeof server.cwd === "string" ? ["cwd"] : []];
|
|
52971
|
+
if (dropped.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets ${dropped.join(" and ")}, which Xum's mcp.jsonc cannot express; the server is written without it (Xum starts stdio servers with your shell environment in the workspace directory).`);
|
|
52972
|
+
return Object.keys(settings).length > 0 ? {
|
|
52973
|
+
command,
|
|
52974
|
+
...settings
|
|
52975
|
+
} : command;
|
|
52976
|
+
}
|
|
52977
|
+
/** Copy only the string values of a header map (secret references have no canonical form). */
|
|
52978
|
+
function copyStringHeaders(value) {
|
|
52979
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
52980
|
+
const result = {};
|
|
52981
|
+
for (const [key, entry] of Object.entries(value)) if (!PROTOTYPE_POLLUTION_KEYS.has(key) && typeof entry === "string") result[key] = entry;
|
|
52982
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
52983
|
+
}
|
|
52984
|
+
/** Read a Xum `servers` map back into the canonical shape. */
|
|
52985
|
+
function convertFromXumFormat(servers) {
|
|
52986
|
+
if (!isPlainObject$1(servers)) return {};
|
|
52987
|
+
const result = {};
|
|
52988
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
52989
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
52990
|
+
if (typeof entry === "string") {
|
|
52991
|
+
result[name] = splitCommandString(entry);
|
|
52992
|
+
continue;
|
|
52993
|
+
}
|
|
52994
|
+
if (!isPlainObject$1(entry)) continue;
|
|
52995
|
+
const server = {};
|
|
52996
|
+
if (typeof entry.url === "string") {
|
|
52997
|
+
if (entry.transport === "http" || entry.transport === "sse") server.type = entry.transport;
|
|
52998
|
+
server.url = entry.url;
|
|
52999
|
+
const headers = copyStringHeaders(entry.headers);
|
|
53000
|
+
if (headers !== void 0) server.headers = headers;
|
|
53001
|
+
} else if (typeof entry.command === "string") Object.assign(server, splitCommandString(entry.command));
|
|
53002
|
+
else continue;
|
|
53003
|
+
if (entry.disabled === true) server.disabled = true;
|
|
53004
|
+
if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
|
|
53005
|
+
result[name] = server;
|
|
53006
|
+
}
|
|
53007
|
+
return result;
|
|
53008
|
+
}
|
|
53009
|
+
/**
|
|
53010
|
+
* Xum MCP configuration.
|
|
53011
|
+
*
|
|
53012
|
+
* Xum reads `~/.xum/mcp.jsonc` (user) and the repo override `.xum/mcp.jsonc`
|
|
53013
|
+
* (project; a server there wins over the user one of the same name). Both are
|
|
53014
|
+
* a JSONC document with a `servers` map:
|
|
53015
|
+
*
|
|
53016
|
+
* - a stdio server is a shell command string (the object form `{ command }`
|
|
53017
|
+
* carries `disabled` / `toolAllowlist`); Xum's mcp.jsonc has no `env` or
|
|
53018
|
+
* `cwd`, so those are dropped with a warning;
|
|
53019
|
+
* - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
|
|
53020
|
+
* (the transport defaults to auto-detection).
|
|
53021
|
+
*
|
|
53022
|
+
* rulesync owns only `servers`; other top-level keys (Xum writes
|
|
53023
|
+
* `enabledPluginServers` into the user file) are preserved. The project file
|
|
53024
|
+
* may be removed by `--delete`; the user file never is.
|
|
53025
|
+
*
|
|
53026
|
+
* @see https://xum.cdr.dev/config/mcp-servers
|
|
53027
|
+
* @see https://github.com/coder/xum/blob/main/src/node/services/mcpConfigService.ts
|
|
53028
|
+
*/
|
|
53029
|
+
var XumMcp = class XumMcp extends ToolMcp {
|
|
53030
|
+
json;
|
|
53031
|
+
constructor(params) {
|
|
53032
|
+
super(params);
|
|
53033
|
+
this.json = parseSharedConfig({
|
|
53034
|
+
format: "jsonc",
|
|
53035
|
+
fileContent: this.fileContent,
|
|
53036
|
+
filePath: (0, node_path.join)(this.relativeDirPath, this.relativeFilePath),
|
|
53037
|
+
invalidRootPolicy: "error",
|
|
53038
|
+
jsoncParseErrors: "error"
|
|
53039
|
+
});
|
|
53040
|
+
}
|
|
53041
|
+
getJson() {
|
|
53042
|
+
return this.json;
|
|
53043
|
+
}
|
|
53044
|
+
isDeletable() {
|
|
53045
|
+
return !this.global;
|
|
53046
|
+
}
|
|
53047
|
+
static getSettablePaths(_options = {}) {
|
|
53048
|
+
return {
|
|
53049
|
+
relativeDirPath: XUM_DIR,
|
|
53050
|
+
relativeFilePath: XUM_MCP_FILE_NAME
|
|
53051
|
+
};
|
|
53052
|
+
}
|
|
53053
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
53054
|
+
const paths = this.getSettablePaths({ global });
|
|
53055
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? EMPTY_XUM_MCP_CONFIG;
|
|
53056
|
+
return new XumMcp({
|
|
53057
|
+
outputRoot,
|
|
53058
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53059
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53060
|
+
fileContent,
|
|
53061
|
+
validate,
|
|
53062
|
+
global
|
|
53063
|
+
});
|
|
53064
|
+
}
|
|
53065
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
53066
|
+
const paths = this.getSettablePaths({ global });
|
|
53067
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
53068
|
+
const existingContent = await readFileContentOrNull(filePath) ?? EMPTY_XUM_MCP_CONFIG;
|
|
53069
|
+
const servers = {};
|
|
53070
|
+
for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
|
|
53071
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53072
|
+
const entry = convertServerToXum({
|
|
53073
|
+
name,
|
|
53074
|
+
server,
|
|
53075
|
+
logger
|
|
53076
|
+
});
|
|
53077
|
+
if (entry !== null) servers[name] = entry;
|
|
53078
|
+
}
|
|
53079
|
+
return new XumMcp({
|
|
53080
|
+
outputRoot,
|
|
53081
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53082
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53083
|
+
fileContent: applySharedConfigPatch({
|
|
53084
|
+
fileKey: sharedConfigFileKey(paths),
|
|
53085
|
+
feature: "mcp",
|
|
53086
|
+
existingContent,
|
|
53087
|
+
patch: { servers },
|
|
53088
|
+
filePath,
|
|
53089
|
+
logger
|
|
53090
|
+
}),
|
|
53091
|
+
validate,
|
|
53092
|
+
global
|
|
53093
|
+
});
|
|
53094
|
+
}
|
|
53095
|
+
toRulesyncMcp() {
|
|
53096
|
+
const mcpServers = convertFromXumFormat(this.json.servers);
|
|
53097
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
53098
|
+
}
|
|
53099
|
+
validate() {
|
|
53100
|
+
return {
|
|
53101
|
+
success: true,
|
|
53102
|
+
error: null
|
|
53103
|
+
};
|
|
53104
|
+
}
|
|
53105
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
53106
|
+
return new XumMcp({
|
|
53107
|
+
outputRoot,
|
|
53108
|
+
relativeDirPath,
|
|
53109
|
+
relativeFilePath,
|
|
53110
|
+
fileContent: "{}",
|
|
53111
|
+
validate: false,
|
|
53112
|
+
global
|
|
53113
|
+
});
|
|
53114
|
+
}
|
|
53115
|
+
};
|
|
53116
|
+
//#endregion
|
|
52033
53117
|
//#region src/features/mcp/zcode-mcp.ts
|
|
52034
53118
|
/**
|
|
52035
53119
|
* Single spelling of the config.json codec/policy, matching the
|
|
@@ -52291,8 +53375,8 @@ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
|
|
|
52291
53375
|
* and unknown keys pass through untouched so an import keeps whatever a
|
|
52292
53376
|
* hand-authored entry declared. `timeoutMs` is lifted back into the canonical
|
|
52293
53377
|
* `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
|
|
53378
|
+
* has already dropped that one, so `timeoutMs` replaces it. Any record
|
|
53379
|
+
* `oauth` is imported as the namespaced `zcodeOauth`, so a client secret is
|
|
52296
53380
|
* never emitted to another tool. Generation back out is a whitelist, so only
|
|
52297
53381
|
* the keys `convertToZcodeFormat` writes are re-emitted.
|
|
52298
53382
|
*
|
|
@@ -52318,7 +53402,7 @@ function convertFromZcodeFormat({ zcodeServers }) {
|
|
|
52318
53402
|
continue;
|
|
52319
53403
|
}
|
|
52320
53404
|
if (key === "timeout" && typeof config.timeoutMs === "number") continue;
|
|
52321
|
-
if (key === "oauth" &&
|
|
53405
|
+
if (key === "oauth" && isRecord$1(value)) {
|
|
52322
53406
|
converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
|
|
52323
53407
|
continue;
|
|
52324
53408
|
}
|
|
@@ -52964,6 +54048,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
52964
54048
|
supportsDisabledTools: true
|
|
52965
54049
|
}
|
|
52966
54050
|
}],
|
|
54051
|
+
["firebender", {
|
|
54052
|
+
class: FirebenderMcp,
|
|
54053
|
+
meta: {
|
|
54054
|
+
supportsProject: true,
|
|
54055
|
+
supportsGlobal: true,
|
|
54056
|
+
supportsEnabledTools: false,
|
|
54057
|
+
supportsDisabledTools: false
|
|
54058
|
+
}
|
|
54059
|
+
}],
|
|
52967
54060
|
["goose", {
|
|
52968
54061
|
class: GooseMcp,
|
|
52969
54062
|
meta: {
|
|
@@ -53234,6 +54327,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
53234
54327
|
supportsDisabledTools: false
|
|
53235
54328
|
}
|
|
53236
54329
|
}],
|
|
54330
|
+
["xum", {
|
|
54331
|
+
class: XumMcp,
|
|
54332
|
+
meta: {
|
|
54333
|
+
supportsProject: true,
|
|
54334
|
+
supportsGlobal: true,
|
|
54335
|
+
supportsEnabledTools: true,
|
|
54336
|
+
supportsDisabledTools: false
|
|
54337
|
+
}
|
|
54338
|
+
}],
|
|
53237
54339
|
["devin", {
|
|
53238
54340
|
class: DevinMcp,
|
|
53239
54341
|
meta: {
|
|
@@ -76445,6 +77547,11 @@ const TOOL_DISPLAY = [
|
|
|
76445
77547
|
label: "Factory Droid",
|
|
76446
77548
|
group: "ai"
|
|
76447
77549
|
},
|
|
77550
|
+
{
|
|
77551
|
+
key: "firebender",
|
|
77552
|
+
label: "Firebender",
|
|
77553
|
+
group: "ai"
|
|
77554
|
+
},
|
|
76448
77555
|
{
|
|
76449
77556
|
key: "opencode",
|
|
76450
77557
|
label: "OpenCode",
|
|
@@ -76600,6 +77707,11 @@ const TOOL_DISPLAY = [
|
|
|
76600
77707
|
label: "JetBrains AI Assistant",
|
|
76601
77708
|
group: "ai"
|
|
76602
77709
|
},
|
|
77710
|
+
{
|
|
77711
|
+
key: "androidstudio",
|
|
77712
|
+
label: "Gemini in Android Studio",
|
|
77713
|
+
group: "ai"
|
|
77714
|
+
},
|
|
76603
77715
|
{
|
|
76604
77716
|
key: "junie",
|
|
76605
77717
|
label: "JetBrains Junie",
|
|
@@ -76635,6 +77747,11 @@ const TOOL_DISPLAY = [
|
|
|
76635
77747
|
label: "Warp Agent CLI",
|
|
76636
77748
|
group: "ai"
|
|
76637
77749
|
},
|
|
77750
|
+
{
|
|
77751
|
+
key: "xum",
|
|
77752
|
+
label: "Xum",
|
|
77753
|
+
group: "ai"
|
|
77754
|
+
},
|
|
76638
77755
|
{
|
|
76639
77756
|
key: "replit",
|
|
76640
77757
|
label: "Replit",
|
|
@@ -77779,6 +78896,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
|
|
|
77779
78896
|
}
|
|
77780
78897
|
};
|
|
77781
78898
|
//#endregion
|
|
78899
|
+
//#region src/features/skills/androidstudio-skill.ts
|
|
78900
|
+
/**
|
|
78901
|
+
* Gemini in Android Studio skills follow the Agent Skills open standard
|
|
78902
|
+
* (`<name>/SKILL.md` with `name` and `description` frontmatter), so this target
|
|
78903
|
+
* reuses {@link AgentsSkillsSkill} with Android Studio's own location:
|
|
78904
|
+
* `.android-studio/skills/` at the project root (project) or in the home
|
|
78905
|
+
* directory (global). The shared `.agents/skills/` location Android Studio also
|
|
78906
|
+
* reads belongs to the `agentsskills` target.
|
|
78907
|
+
*
|
|
78908
|
+
* @see https://developer.android.com/studio/gemini/skills
|
|
78909
|
+
*/
|
|
78910
|
+
var AndroidstudioSkill = class extends AgentsSkillsSkill {
|
|
78911
|
+
static getSettablePaths(_options = {}) {
|
|
78912
|
+
return { relativeDirPath: ANDROIDSTUDIO_SKILLS_DIR_PATH };
|
|
78913
|
+
}
|
|
78914
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
78915
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
78916
|
+
return targets.includes("*") || targets.includes("androidstudio");
|
|
78917
|
+
}
|
|
78918
|
+
};
|
|
78919
|
+
//#endregion
|
|
77782
78920
|
//#region src/features/skills/antigravity-plugin-skill.ts
|
|
77783
78921
|
var AntigravityPluginSkill = class extends AntigravityIdeSkill {
|
|
77784
78922
|
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
@@ -81247,6 +82385,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
81247
82385
|
}
|
|
81248
82386
|
};
|
|
81249
82387
|
//#endregion
|
|
82388
|
+
//#region src/features/skills/firebender-skill.ts
|
|
82389
|
+
/**
|
|
82390
|
+
* Firebender skills follow the Agent Skills layout (`<name>/SKILL.md` with the
|
|
82391
|
+
* required `name` and `description` frontmatter, plus optional supporting
|
|
82392
|
+
* files), so this target reuses {@link AgentsSkillsSkill} with Firebender's
|
|
82393
|
+
* recommended locations: `.firebender/skills/` (project) and
|
|
82394
|
+
* `~/.firebender/skills/` (user).
|
|
82395
|
+
*
|
|
82396
|
+
* @see https://docs.firebender.com/api-reference/skills
|
|
82397
|
+
*/
|
|
82398
|
+
var FirebenderSkill = class extends AgentsSkillsSkill {
|
|
82399
|
+
static getSettablePaths(_options = {}) {
|
|
82400
|
+
return { relativeDirPath: FIREBENDER_SKILLS_DIR_PATH };
|
|
82401
|
+
}
|
|
82402
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
82403
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
82404
|
+
return targets.includes("*") || targets.includes("firebender");
|
|
82405
|
+
}
|
|
82406
|
+
};
|
|
82407
|
+
//#endregion
|
|
81250
82408
|
//#region src/features/skills/gitlabduo-skill.ts
|
|
81251
82409
|
/**
|
|
81252
82410
|
* GitLab Duo skills follow the Agent Skills standard (`name` and `description`
|
|
@@ -84782,6 +85940,27 @@ var WarpSkill = class WarpSkill extends ToolSkill {
|
|
|
84782
85940
|
}
|
|
84783
85941
|
};
|
|
84784
85942
|
//#endregion
|
|
85943
|
+
//#region src/features/skills/xum-skill.ts
|
|
85944
|
+
/**
|
|
85945
|
+
* Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
|
|
85946
|
+
* required `name` and `description` frontmatter; the directory name is
|
|
85947
|
+
* lowercase letters, digits and single hyphens up to 64 characters, and
|
|
85948
|
+
* `name` must match it), so this target reuses {@link AgentsSkillsSkill} with
|
|
85949
|
+
* Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
|
|
85950
|
+
* Within one directory `.xum/skills` wins over `.agents/skills`.
|
|
85951
|
+
*
|
|
85952
|
+
* @see https://xum.cdr.dev/agents/agent-skills
|
|
85953
|
+
*/
|
|
85954
|
+
var XumSkill = class extends AgentsSkillsSkill {
|
|
85955
|
+
static getSettablePaths(_options = {}) {
|
|
85956
|
+
return { relativeDirPath: XUM_SKILLS_DIR_PATH };
|
|
85957
|
+
}
|
|
85958
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
85959
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
85960
|
+
return targets.includes("*") || targets.includes("xum");
|
|
85961
|
+
}
|
|
85962
|
+
};
|
|
85963
|
+
//#endregion
|
|
84785
85964
|
//#region src/features/skills/zcode-skill.ts
|
|
84786
85965
|
const ZcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
84787
85966
|
name: zod_mini.z.string(),
|
|
@@ -85197,6 +86376,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85197
86376
|
supportsGlobal: false
|
|
85198
86377
|
}
|
|
85199
86378
|
}],
|
|
86379
|
+
["androidstudio", {
|
|
86380
|
+
class: AndroidstudioSkill,
|
|
86381
|
+
meta: {
|
|
86382
|
+
supportsProject: true,
|
|
86383
|
+
supportsSimulated: false,
|
|
86384
|
+
supportsGlobal: true
|
|
86385
|
+
}
|
|
86386
|
+
}],
|
|
85200
86387
|
["amp", {
|
|
85201
86388
|
class: AmpSkill,
|
|
85202
86389
|
meta: {
|
|
@@ -85382,6 +86569,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85382
86569
|
supportsGlobal: true
|
|
85383
86570
|
}
|
|
85384
86571
|
}],
|
|
86572
|
+
["firebender", {
|
|
86573
|
+
class: FirebenderSkill,
|
|
86574
|
+
meta: {
|
|
86575
|
+
supportsProject: true,
|
|
86576
|
+
supportsSimulated: false,
|
|
86577
|
+
supportsGlobal: true
|
|
86578
|
+
}
|
|
86579
|
+
}],
|
|
85385
86580
|
["goose", {
|
|
85386
86581
|
class: GooseSkill,
|
|
85387
86582
|
meta: {
|
|
@@ -85632,6 +86827,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85632
86827
|
supportsGlobal: true
|
|
85633
86828
|
}
|
|
85634
86829
|
}],
|
|
86830
|
+
["xum", {
|
|
86831
|
+
class: XumSkill,
|
|
86832
|
+
meta: {
|
|
86833
|
+
supportsProject: true,
|
|
86834
|
+
supportsSimulated: false,
|
|
86835
|
+
supportsGlobal: true
|
|
86836
|
+
}
|
|
86837
|
+
}],
|
|
85635
86838
|
["devin", {
|
|
85636
86839
|
class: DevinSkill,
|
|
85637
86840
|
meta: {
|
|
@@ -89478,6 +90681,338 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
|
|
|
89478
90681
|
}
|
|
89479
90682
|
};
|
|
89480
90683
|
//#endregion
|
|
90684
|
+
//#region src/features/subagents/firebender-subagent.ts
|
|
90685
|
+
/**
|
|
90686
|
+
* Frontmatter of a Firebender agent file. `tools` is a comma-separated string
|
|
90687
|
+
* of tool groups (`read`, `edit`, `execution`, `other`) and MCP tools
|
|
90688
|
+
* (`mcp_<server>_<tool>`); `model` is `small`, `medium`, `large`, `inherit` or a
|
|
90689
|
+
* model ID; `callable` lets the main agent delegate to it. Unknown keys are
|
|
90690
|
+
* preserved.
|
|
90691
|
+
*
|
|
90692
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
90693
|
+
*/
|
|
90694
|
+
const FirebenderSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
90695
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
90696
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
90697
|
+
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
90698
|
+
icon: zod_mini.z.optional(zod_mini.z.string()),
|
|
90699
|
+
tools: zod_mini.z.optional(zod_mini.z.string()),
|
|
90700
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
90701
|
+
callable: zod_mini.z.optional(zod_mini.z.boolean())
|
|
90702
|
+
});
|
|
90703
|
+
/**
|
|
90704
|
+
* The `agents` entry Firebender resolves to an agent file rulesync writes:
|
|
90705
|
+
* relative to the project root for the project config, and to `~/.firebender`
|
|
90706
|
+
* for the personal one.
|
|
90707
|
+
*/
|
|
90708
|
+
function toAgentsEntry({ relativeFilePath, global }) {
|
|
90709
|
+
const fileName = toPosixPath(relativeFilePath);
|
|
90710
|
+
return global ? node_path.posix.join(FIREBENDER_AGENTS_DIR_NAME, fileName) : node_path.posix.join(toPosixPath(FIREBENDER_AGENTS_DIR_PATH), fileName);
|
|
90711
|
+
}
|
|
90712
|
+
/**
|
|
90713
|
+
* The agent file name an existing `agents` entry points at when it lies
|
|
90714
|
+
* directly inside the directory rulesync writes agents to
|
|
90715
|
+
* (`.firebender/agents/` in the project; `agents/` or `~/.firebender/agents/`
|
|
90716
|
+
* in the personal config), or `undefined` for any other entry
|
|
90717
|
+
* (`~/firebender-agents/...`, an absolute path, a nested path).
|
|
90718
|
+
*/
|
|
90719
|
+
function managedAgentFileName({ entry, global }) {
|
|
90720
|
+
const normalized = toPosixPath(entry).replace(/^\.\//, "");
|
|
90721
|
+
const prefix = global ? `${FIREBENDER_AGENTS_DIR_NAME}/` : `${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
|
|
90722
|
+
const homePrefix = `~/${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
|
|
90723
|
+
const rest = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : global && normalized.startsWith(homePrefix) ? normalized.slice(homePrefix.length) : void 0;
|
|
90724
|
+
return rest !== void 0 && rest.endsWith(".md") && !rest.includes("/") ? rest : void 0;
|
|
90725
|
+
}
|
|
90726
|
+
/**
|
|
90727
|
+
* Subagent generator for Firebender.
|
|
90728
|
+
*
|
|
90729
|
+
* Each agent is a Markdown file with YAML frontmatter, written to
|
|
90730
|
+
* `.firebender/agents/<name>.md` (project) or `~/.firebender/agents/<name>.md`
|
|
90731
|
+
* (global). Firebender only loads agents listed in the `agents` array of
|
|
90732
|
+
* `firebender.json`, so generation also registers every file there — see
|
|
90733
|
+
* {@link FirebenderAgentsRegistry}. Rulesync subagents are meant to be
|
|
90734
|
+
* delegated to, so `callable: true` is written unless the `firebender:`
|
|
90735
|
+
* section says otherwise.
|
|
90736
|
+
*
|
|
90737
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
90738
|
+
*/
|
|
90739
|
+
var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
90740
|
+
frontmatter;
|
|
90741
|
+
body;
|
|
90742
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
90743
|
+
if (rest.validate !== false) {
|
|
90744
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
90745
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
90746
|
+
}
|
|
90747
|
+
super({ ...rest });
|
|
90748
|
+
this.frontmatter = frontmatter;
|
|
90749
|
+
this.body = body;
|
|
90750
|
+
}
|
|
90751
|
+
static getSettablePaths(_options = {}) {
|
|
90752
|
+
return { relativeDirPath: FIREBENDER_AGENTS_DIR_PATH };
|
|
90753
|
+
}
|
|
90754
|
+
/**
|
|
90755
|
+
* Generation also read-modify-writes the `agents` key of `firebender.json`,
|
|
90756
|
+
* so the write is declared for the shared-file order derivation.
|
|
90757
|
+
*/
|
|
90758
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
90759
|
+
return [{
|
|
90760
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
90761
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
90762
|
+
}];
|
|
90763
|
+
}
|
|
90764
|
+
getFrontmatter() {
|
|
90765
|
+
return this.frontmatter;
|
|
90766
|
+
}
|
|
90767
|
+
getBody() {
|
|
90768
|
+
return this.body;
|
|
90769
|
+
}
|
|
90770
|
+
toRulesyncSubagent() {
|
|
90771
|
+
const { name, description, callable, ...rest } = this.frontmatter;
|
|
90772
|
+
const firebenderSection = {
|
|
90773
|
+
...rest,
|
|
90774
|
+
...callable !== true && { callable: callable ?? false }
|
|
90775
|
+
};
|
|
90776
|
+
return new RulesyncSubagent({
|
|
90777
|
+
outputRoot: ".",
|
|
90778
|
+
frontmatter: {
|
|
90779
|
+
targets: ["*"],
|
|
90780
|
+
name: name ?? (0, node_path.basename)(this.getRelativeFilePath(), ".md"),
|
|
90781
|
+
description,
|
|
90782
|
+
...Object.keys(firebenderSection).length > 0 && { firebender: firebenderSection }
|
|
90783
|
+
},
|
|
90784
|
+
body: this.body,
|
|
90785
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
90786
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
90787
|
+
validate: true
|
|
90788
|
+
});
|
|
90789
|
+
}
|
|
90790
|
+
/**
|
|
90791
|
+
* Every targeted subagent becomes its own agent file, plus one
|
|
90792
|
+
* {@link FirebenderAgentsRegistry} that lists them in `firebender.json`.
|
|
90793
|
+
* The registry is emitted even when no subagent is targeted, so agents a
|
|
90794
|
+
* previous run registered are retracted once their files are gone. The
|
|
90795
|
+
* agent directory is listed up front so the registry can tell a registered
|
|
90796
|
+
* file that still exists (kept, whoever wrote it) from a stale one.
|
|
90797
|
+
*/
|
|
90798
|
+
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
90799
|
+
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
90800
|
+
const agentsDirEscapesRoot = await writablePathEscapesRoot({
|
|
90801
|
+
rootPath: outputRoot,
|
|
90802
|
+
targetPath: (0, node_path.join)(outputRoot, agentsDirPath, "agent.md")
|
|
90803
|
+
});
|
|
90804
|
+
const existingAgentFileNames = new Set(agentsDirEscapesRoot ? [] : await listDirectoryEntryNames((0, node_path.join)(outputRoot, agentsDirPath)));
|
|
90805
|
+
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
90806
|
+
outputRoot,
|
|
90807
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
90808
|
+
rulesyncSubagent,
|
|
90809
|
+
global
|
|
90810
|
+
}));
|
|
90811
|
+
const registry = new FirebenderAgentsRegistry({
|
|
90812
|
+
outputRoot,
|
|
90813
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
90814
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME,
|
|
90815
|
+
entries: subagents.map((subagent) => toAgentsEntry({
|
|
90816
|
+
relativeFilePath: subagent.getRelativeFilePath(),
|
|
90817
|
+
global
|
|
90818
|
+
})),
|
|
90819
|
+
existingAgentFileNames,
|
|
90820
|
+
deleteOrphans,
|
|
90821
|
+
frozen: agentsDirEscapesRoot,
|
|
90822
|
+
global,
|
|
90823
|
+
logger
|
|
90824
|
+
});
|
|
90825
|
+
return [...subagents, registry];
|
|
90826
|
+
}
|
|
90827
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
90828
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
90829
|
+
const firebenderSection = rulesyncFrontmatter.firebender ?? {};
|
|
90830
|
+
const frontmatter = {
|
|
90831
|
+
name: rulesyncFrontmatter.name,
|
|
90832
|
+
...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
|
|
90833
|
+
callable: true,
|
|
90834
|
+
...firebenderSection
|
|
90835
|
+
};
|
|
90836
|
+
const body = rulesyncSubagent.getBody();
|
|
90837
|
+
return new FirebenderSubagent({
|
|
90838
|
+
outputRoot,
|
|
90839
|
+
frontmatter,
|
|
90840
|
+
body,
|
|
90841
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
90842
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
90843
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
|
|
90844
|
+
validate,
|
|
90845
|
+
global
|
|
90846
|
+
});
|
|
90847
|
+
}
|
|
90848
|
+
validate() {
|
|
90849
|
+
if (!this.frontmatter) return {
|
|
90850
|
+
success: true,
|
|
90851
|
+
error: null
|
|
90852
|
+
};
|
|
90853
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
90854
|
+
if (result.success) return {
|
|
90855
|
+
success: true,
|
|
90856
|
+
error: null
|
|
90857
|
+
};
|
|
90858
|
+
return {
|
|
90859
|
+
success: false,
|
|
90860
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
90861
|
+
};
|
|
90862
|
+
}
|
|
90863
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
90864
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
90865
|
+
rulesyncSubagent,
|
|
90866
|
+
toolTarget: "firebender"
|
|
90867
|
+
});
|
|
90868
|
+
}
|
|
90869
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
90870
|
+
const paths = this.getSettablePaths({ global });
|
|
90871
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
90872
|
+
const fileContent = await readFileContent(filePath);
|
|
90873
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
90874
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
90875
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
90876
|
+
return new FirebenderSubagent({
|
|
90877
|
+
outputRoot,
|
|
90878
|
+
relativeDirPath: paths.relativeDirPath,
|
|
90879
|
+
relativeFilePath,
|
|
90880
|
+
frontmatter: result.data,
|
|
90881
|
+
body: body.trim(),
|
|
90882
|
+
fileContent,
|
|
90883
|
+
validate,
|
|
90884
|
+
global
|
|
90885
|
+
});
|
|
90886
|
+
}
|
|
90887
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
90888
|
+
return new FirebenderSubagent({
|
|
90889
|
+
outputRoot,
|
|
90890
|
+
relativeDirPath,
|
|
90891
|
+
relativeFilePath,
|
|
90892
|
+
frontmatter: {},
|
|
90893
|
+
body: "",
|
|
90894
|
+
fileContent: "",
|
|
90895
|
+
validate: false
|
|
90896
|
+
});
|
|
90897
|
+
}
|
|
90898
|
+
};
|
|
90899
|
+
/**
|
|
90900
|
+
* The `agents` array of `firebender.json`, which Firebender requires to load
|
|
90901
|
+
* an agent file. Every generated agent is registered. An existing entry that
|
|
90902
|
+
* points into rulesync's agent directory is kept while its file exists (a
|
|
90903
|
+
* hand-written agent there stays registered) and dropped, with a warning, once
|
|
90904
|
+
* the file is gone. Under `--delete` the orphan sweep removes every agent file
|
|
90905
|
+
* there that this run does not generate, but only after this file is written,
|
|
90906
|
+
* so in that mode such entries are dropped here too and a single run
|
|
90907
|
+
* converges. Every other entry and every other key of the file is kept.
|
|
90908
|
+
* The array is merged into the file on disk at write time and the file itself
|
|
90909
|
+
* is never deleted.
|
|
90910
|
+
*/
|
|
90911
|
+
var FirebenderAgentsRegistry = class extends ToolSubagent {
|
|
90912
|
+
entries;
|
|
90913
|
+
existingAgentFileNames;
|
|
90914
|
+
deleteOrphans;
|
|
90915
|
+
frozen;
|
|
90916
|
+
logger;
|
|
90917
|
+
mergedContent = null;
|
|
90918
|
+
constructor({ entries, existingAgentFileNames = /* @__PURE__ */ new Set(), deleteOrphans = false, frozen = false, logger, ...rest }) {
|
|
90919
|
+
super({
|
|
90920
|
+
...rest,
|
|
90921
|
+
fileContent: ""
|
|
90922
|
+
});
|
|
90923
|
+
this.entries = entries;
|
|
90924
|
+
this.existingAgentFileNames = existingAgentFileNames;
|
|
90925
|
+
this.deleteOrphans = deleteOrphans;
|
|
90926
|
+
this.frozen = frozen;
|
|
90927
|
+
this.logger = logger;
|
|
90928
|
+
}
|
|
90929
|
+
getEntries() {
|
|
90930
|
+
return this.entries;
|
|
90931
|
+
}
|
|
90932
|
+
isDeletable() {
|
|
90933
|
+
return false;
|
|
90934
|
+
}
|
|
90935
|
+
shouldMergeExistingFileContent() {
|
|
90936
|
+
return true;
|
|
90937
|
+
}
|
|
90938
|
+
setFileContent(newFileContent) {
|
|
90939
|
+
this.mergedContent = this.mergeInto(newFileContent);
|
|
90940
|
+
}
|
|
90941
|
+
getFileContent() {
|
|
90942
|
+
if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
|
|
90943
|
+
return this.mergedContent;
|
|
90944
|
+
}
|
|
90945
|
+
mergeInto(existingContent) {
|
|
90946
|
+
const paths = {
|
|
90947
|
+
relativeDirPath: this.getRelativeDirPath(),
|
|
90948
|
+
relativeFilePath: this.getRelativeFilePath()
|
|
90949
|
+
};
|
|
90950
|
+
const filePath = (0, node_path.join)(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
|
|
90951
|
+
const existing = parseSharedConfig({
|
|
90952
|
+
format: "json",
|
|
90953
|
+
fileContent: existingContent,
|
|
90954
|
+
filePath,
|
|
90955
|
+
invalidRootPolicy: "error"
|
|
90956
|
+
});
|
|
90957
|
+
if (this.frozen) return applySharedConfigPatch({
|
|
90958
|
+
fileKey: sharedConfigFileKey(paths),
|
|
90959
|
+
feature: "subagents",
|
|
90960
|
+
existingContent,
|
|
90961
|
+
patch: { agents: existing.agents },
|
|
90962
|
+
filePath,
|
|
90963
|
+
logger: this.logger
|
|
90964
|
+
});
|
|
90965
|
+
const existingEntries = Array.isArray(existing.agents) ? existing.agents : [];
|
|
90966
|
+
const generatedByFileName = new Map(this.entries.map((entry) => [managedAgentFileName({
|
|
90967
|
+
entry,
|
|
90968
|
+
global: this.global
|
|
90969
|
+
}) ?? entry, entry]));
|
|
90970
|
+
const agents = [];
|
|
90971
|
+
const placed = /* @__PURE__ */ new Set();
|
|
90972
|
+
const dropped = [];
|
|
90973
|
+
for (const entry of existingEntries) {
|
|
90974
|
+
const fileName = typeof entry === "string" ? managedAgentFileName({
|
|
90975
|
+
entry,
|
|
90976
|
+
global: this.global
|
|
90977
|
+
}) : void 0;
|
|
90978
|
+
if (fileName === void 0) {
|
|
90979
|
+
agents.push(entry);
|
|
90980
|
+
continue;
|
|
90981
|
+
}
|
|
90982
|
+
const generated = generatedByFileName.get(fileName);
|
|
90983
|
+
if (generated !== void 0) {
|
|
90984
|
+
if (!placed.has(fileName)) {
|
|
90985
|
+
agents.push(generated);
|
|
90986
|
+
placed.add(fileName);
|
|
90987
|
+
}
|
|
90988
|
+
} else if (!this.deleteOrphans && this.existingAgentFileNames.has(fileName)) agents.push(entry);
|
|
90989
|
+
else dropped.push(String(entry));
|
|
90990
|
+
}
|
|
90991
|
+
for (const [fileName, entry] of generatedByFileName) if (!placed.has(fileName)) {
|
|
90992
|
+
agents.push(entry);
|
|
90993
|
+
placed.add(fileName);
|
|
90994
|
+
}
|
|
90995
|
+
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."));
|
|
90996
|
+
return applySharedConfigPatch({
|
|
90997
|
+
fileKey: sharedConfigFileKey(paths),
|
|
90998
|
+
feature: "subagents",
|
|
90999
|
+
existingContent,
|
|
91000
|
+
patch: { agents: agents.length > 0 ? agents : void 0 },
|
|
91001
|
+
filePath,
|
|
91002
|
+
logger: this.logger
|
|
91003
|
+
});
|
|
91004
|
+
}
|
|
91005
|
+
toRulesyncSubagent() {
|
|
91006
|
+
throw new Error(`${this.getRelativePathFromCwd()} registers Firebender agents and is not a subagent itself.`);
|
|
91007
|
+
}
|
|
91008
|
+
validate() {
|
|
91009
|
+
return {
|
|
91010
|
+
success: true,
|
|
91011
|
+
error: null
|
|
91012
|
+
};
|
|
91013
|
+
}
|
|
91014
|
+
};
|
|
91015
|
+
//#endregion
|
|
89481
91016
|
//#region src/features/subagents/goose-subagent.ts
|
|
89482
91017
|
/**
|
|
89483
91018
|
* Goose custom agents (v1.34.0+) are Markdown files with YAML frontmatter —
|
|
@@ -89765,13 +91300,13 @@ function subagentSlug(relativeFilePath) {
|
|
|
89765
91300
|
return (0, node_path.basename)(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
89766
91301
|
}
|
|
89767
91302
|
function hermesCommandName(slug) {
|
|
89768
|
-
return `
|
|
91303
|
+
return hermesSlashName(`rulesync-subagent-${slug}`);
|
|
89769
91304
|
}
|
|
89770
91305
|
function getPluginManifestContent() {
|
|
89771
91306
|
return [
|
|
89772
91307
|
"name: rulesync-subagents",
|
|
89773
91308
|
"version: \"1.0.0\"",
|
|
89774
|
-
"description: Exposes RuleSync subagents as Hermes
|
|
91309
|
+
"description: Exposes RuleSync subagents as Hermes slash commands that queue a delegation prompt.",
|
|
89775
91310
|
""
|
|
89776
91311
|
].join("\n");
|
|
89777
91312
|
}
|
|
@@ -89779,9 +91314,12 @@ function getPluginInitContent() {
|
|
|
89779
91314
|
return `"""RuleSync-generated Hermes subagent commands."""
|
|
89780
91315
|
|
|
89781
91316
|
import json
|
|
91317
|
+
import logging
|
|
91318
|
+
import re
|
|
89782
91319
|
from pathlib import Path
|
|
89783
91320
|
|
|
89784
91321
|
|
|
91322
|
+
logger = logging.getLogger(__name__)
|
|
89785
91323
|
SUBAGENTS_DIR = Path(__file__).resolve().parents[2] / "rulesync" / "subagents"
|
|
89786
91324
|
|
|
89787
91325
|
|
|
@@ -89801,52 +91339,71 @@ def _load_subagents():
|
|
|
89801
91339
|
return subagents
|
|
89802
91340
|
|
|
89803
91341
|
|
|
89804
|
-
def
|
|
91342
|
+
def _command_name(subagent, slug):
|
|
91343
|
+
# The spec carries the name Rulesync normalized; Hermes' messaging gateway
|
|
91344
|
+
# resolves plugin commands with "_" turned into "-", so keep it hyphenated.
|
|
91345
|
+
hermes = subagent.get("hermes")
|
|
91346
|
+
name = hermes.get("command") if isinstance(hermes, dict) else None
|
|
91347
|
+
name = name or f"rulesync-subagent-{slug}"
|
|
91348
|
+
# Mirrors hermesSlashName() in src/features/shared/hermes-plugin-commands.ts.
|
|
91349
|
+
name = re.sub(r"[ _]", "-", str(name).lower())
|
|
91350
|
+
name = re.sub(r"[^a-z0-9-]", "", name)
|
|
91351
|
+
return re.sub(r"-{2,}", "-", name).strip("-")
|
|
91352
|
+
|
|
91353
|
+
|
|
91354
|
+
${getHermesQueueUserMessagePython({ pluginName: "rulesync-subagents" })}
|
|
91355
|
+
|
|
91356
|
+
def _register_subagent(ctx, subagent, registered):
|
|
89805
91357
|
slug = subagent.get("slug")
|
|
89806
91358
|
if not slug:
|
|
89807
91359
|
return
|
|
89808
91360
|
|
|
89809
|
-
command_name =
|
|
91361
|
+
command_name = _command_name(subagent, slug)
|
|
91362
|
+
if command_name in registered:
|
|
91363
|
+
logger.warning(
|
|
91364
|
+
"rulesync-subagents: %s and %s both map to /%s; keeping the first.",
|
|
91365
|
+
registered[command_name],
|
|
91366
|
+
subagent.get("_path"),
|
|
91367
|
+
command_name,
|
|
91368
|
+
)
|
|
91369
|
+
return
|
|
91370
|
+
registered[command_name] = subagent.get("_path")
|
|
91371
|
+
|
|
89810
91372
|
name = subagent.get("name") or slug
|
|
89811
91373
|
description = subagent.get("description") or f"Delegate work to the {name} RuleSync subagent."
|
|
89812
91374
|
system_prompt = subagent.get("prompt") or ""
|
|
89813
91375
|
|
|
89814
91376
|
def handler(args=None, **kwargs):
|
|
89815
|
-
del kwargs
|
|
89816
91377
|
user_context = ""
|
|
89817
91378
|
if isinstance(args, dict):
|
|
89818
91379
|
user_context = args.get("context") or args.get("task") or args.get("prompt") or ""
|
|
89819
91380
|
elif args is not None:
|
|
89820
91381
|
user_context = str(args)
|
|
89821
91382
|
|
|
89822
|
-
|
|
91383
|
+
# A slash command runs outside any agent turn, so calling delegate_task
|
|
91384
|
+
# here fails with "delegate_task requires a parent agent context".
|
|
91385
|
+
# Queue the brief as a user message instead; the agent then delegates.
|
|
91386
|
+
parts = [
|
|
91387
|
+
f"Use the delegate_task tool to hand the following task to a subagent "
|
|
91388
|
+
f"acting as the {name} RuleSync subagent.",
|
|
91389
|
+
f"Goal: {description}",
|
|
91390
|
+
]
|
|
89823
91391
|
if system_prompt:
|
|
89824
|
-
|
|
91392
|
+
parts.append(f"Subagent instructions:\\n{system_prompt}")
|
|
89825
91393
|
if user_context:
|
|
89826
|
-
|
|
91394
|
+
parts.append(f"Task details:\\n{user_context}")
|
|
89827
91395
|
|
|
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
|
-
)
|
|
91396
|
+
label = f"the {name} subagent task"
|
|
91397
|
+
error = _queue_user_message(ctx, "\\n\\n".join(parts), kwargs, label)
|
|
91398
|
+
return error or f"Queued {label}; the agent will delegate it."
|
|
89843
91399
|
|
|
89844
91400
|
ctx.register_command(command_name, handler, description)
|
|
89845
91401
|
|
|
89846
91402
|
|
|
89847
91403
|
def register(ctx):
|
|
91404
|
+
registered = {}
|
|
89848
91405
|
for subagent in _load_subagents():
|
|
89849
|
-
_register_subagent(ctx, subagent)
|
|
91406
|
+
_register_subagent(ctx, subagent, registered)
|
|
89850
91407
|
`;
|
|
89851
91408
|
}
|
|
89852
91409
|
function getEnabledPluginConfigContent({ currentContent, global }) {
|
|
@@ -89877,7 +91434,7 @@ function getSubagentSpec(rulesyncSubagent) {
|
|
|
89877
91434
|
prompt: rulesyncSubagent.getBody(),
|
|
89878
91435
|
hermes: {
|
|
89879
91436
|
command: hermesCommandName(slug),
|
|
89880
|
-
dispatch: "
|
|
91437
|
+
dispatch: "inject_message"
|
|
89881
91438
|
}
|
|
89882
91439
|
};
|
|
89883
91440
|
}
|
|
@@ -92669,6 +94226,166 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
|
|
|
92669
94226
|
}
|
|
92670
94227
|
};
|
|
92671
94228
|
//#endregion
|
|
94229
|
+
//#region src/features/subagents/xum-subagent.ts
|
|
94230
|
+
/**
|
|
94231
|
+
* The built-in agent a rulesync subagent inherits from when its `xum` block
|
|
94232
|
+
* names neither a `base` nor its own `tools`. Xum gives an agent no tools at
|
|
94233
|
+
* all unless some agent in its `base` chain declares them, so a definition
|
|
94234
|
+
* without either would be a prompt that cannot read or edit anything.
|
|
94235
|
+
*/
|
|
94236
|
+
const XUM_DEFAULT_BASE_AGENT = "exec";
|
|
94237
|
+
/**
|
|
94238
|
+
* Frontmatter of a Xum agent definition. `name` (the display name) is
|
|
94239
|
+
* required; the other documented fields are typed loosely and unknown keys are
|
|
94240
|
+
* kept (looseObject) so a round trip never drops them.
|
|
94241
|
+
*
|
|
94242
|
+
* @see https://xum.cdr.dev/agents
|
|
94243
|
+
*/
|
|
94244
|
+
const XumSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
94245
|
+
name: zod_mini.z.string(),
|
|
94246
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
94247
|
+
base: zod_mini.z.optional(zod_mini.z.string()),
|
|
94248
|
+
disabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
94249
|
+
ui: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
94250
|
+
prompt: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
94251
|
+
subagent: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
94252
|
+
ai: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
94253
|
+
tools: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
94254
|
+
});
|
|
94255
|
+
/**
|
|
94256
|
+
* Subagent generator for Xum (formerly Mux).
|
|
94257
|
+
*
|
|
94258
|
+
* Xum reads custom agent definitions — Markdown with YAML frontmatter — from
|
|
94259
|
+
* the top level of `.xum/agents/` (project) and `~/.xum/agents/` (user); the
|
|
94260
|
+
* body is the agent's instruction prompt. The file name (without `.md`) is the
|
|
94261
|
+
* agent id, so it must be lowercase letters and digits with single `-` / `_`
|
|
94262
|
+
* separators; Xum skips other files with a warning.
|
|
94263
|
+
*
|
|
94264
|
+
* rulesync writes `name` and `description`, spreads the `xum` section (`base`,
|
|
94265
|
+
* `ui`, `prompt`, `subagent`, `ai`, `tools`, ...) on top, and fills two
|
|
94266
|
+
* defaults so the definition works as a delegated subagent out of the box:
|
|
94267
|
+
* `subagent.runnable: true` (required for `task({ agentId })` to spawn it) and,
|
|
94268
|
+
* when the section sets neither `base` nor `tools`, `base: exec`.
|
|
94269
|
+
*
|
|
94270
|
+
* @see https://xum.cdr.dev/agents
|
|
94271
|
+
*/
|
|
94272
|
+
var XumSubagent = class XumSubagent extends ToolSubagent {
|
|
94273
|
+
frontmatter;
|
|
94274
|
+
body;
|
|
94275
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
94276
|
+
if (rest.validate !== false) {
|
|
94277
|
+
const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
94278
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
94279
|
+
}
|
|
94280
|
+
super({ ...rest });
|
|
94281
|
+
this.frontmatter = frontmatter;
|
|
94282
|
+
this.body = body;
|
|
94283
|
+
}
|
|
94284
|
+
static getSettablePaths(_options = {}) {
|
|
94285
|
+
return { relativeDirPath: XUM_AGENTS_DIR_PATH };
|
|
94286
|
+
}
|
|
94287
|
+
getFrontmatter() {
|
|
94288
|
+
return this.frontmatter;
|
|
94289
|
+
}
|
|
94290
|
+
getBody() {
|
|
94291
|
+
return this.body;
|
|
94292
|
+
}
|
|
94293
|
+
toRulesyncSubagent() {
|
|
94294
|
+
const { name, description, ...restFields } = this.frontmatter;
|
|
94295
|
+
return new RulesyncSubagent({
|
|
94296
|
+
outputRoot: ".",
|
|
94297
|
+
frontmatter: {
|
|
94298
|
+
targets: ["*"],
|
|
94299
|
+
name,
|
|
94300
|
+
description,
|
|
94301
|
+
...Object.keys(restFields).length > 0 && { xum: restFields }
|
|
94302
|
+
},
|
|
94303
|
+
body: this.body,
|
|
94304
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
94305
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
94306
|
+
validate: true
|
|
94307
|
+
});
|
|
94308
|
+
}
|
|
94309
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
94310
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
94311
|
+
const xumSection = this.filterToolSpecificSection(rulesyncFrontmatter.xum ?? {}, ["name", "description"]);
|
|
94312
|
+
const subagentSection = xumSection.subagent;
|
|
94313
|
+
const result = XumSubagentFrontmatterSchema.safeParse({
|
|
94314
|
+
name: rulesyncFrontmatter.name,
|
|
94315
|
+
...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
|
|
94316
|
+
...xumSection.base === void 0 && xumSection.tools === void 0 && { base: XUM_DEFAULT_BASE_AGENT },
|
|
94317
|
+
...xumSection,
|
|
94318
|
+
subagent: {
|
|
94319
|
+
runnable: true,
|
|
94320
|
+
...isPlainObject$1(subagentSection) ? subagentSection : {}
|
|
94321
|
+
}
|
|
94322
|
+
});
|
|
94323
|
+
if (!result.success) throw new Error(`Invalid xum subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
94324
|
+
const body = rulesyncSubagent.getBody();
|
|
94325
|
+
return new XumSubagent({
|
|
94326
|
+
outputRoot,
|
|
94327
|
+
frontmatter: result.data,
|
|
94328
|
+
body,
|
|
94329
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
94330
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
94331
|
+
fileContent: stringifyFrontmatter(body, result.data),
|
|
94332
|
+
validate
|
|
94333
|
+
});
|
|
94334
|
+
}
|
|
94335
|
+
validate() {
|
|
94336
|
+
if (!this.frontmatter) return {
|
|
94337
|
+
success: true,
|
|
94338
|
+
error: null
|
|
94339
|
+
};
|
|
94340
|
+
const result = XumSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
94341
|
+
if (result.success) return {
|
|
94342
|
+
success: true,
|
|
94343
|
+
error: null
|
|
94344
|
+
};
|
|
94345
|
+
return {
|
|
94346
|
+
success: false,
|
|
94347
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
94348
|
+
};
|
|
94349
|
+
}
|
|
94350
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
94351
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
94352
|
+
rulesyncSubagent,
|
|
94353
|
+
toolTarget: "xum"
|
|
94354
|
+
});
|
|
94355
|
+
}
|
|
94356
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
94357
|
+
const paths = this.getSettablePaths({ global });
|
|
94358
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
94359
|
+
const fileContent = await readFileContent(filePath);
|
|
94360
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
94361
|
+
const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
94362
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
94363
|
+
return new XumSubagent({
|
|
94364
|
+
outputRoot,
|
|
94365
|
+
relativeDirPath: paths.relativeDirPath,
|
|
94366
|
+
relativeFilePath,
|
|
94367
|
+
frontmatter: result.data,
|
|
94368
|
+
body: body.trim(),
|
|
94369
|
+
fileContent,
|
|
94370
|
+
validate
|
|
94371
|
+
});
|
|
94372
|
+
}
|
|
94373
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
94374
|
+
return new XumSubagent({
|
|
94375
|
+
outputRoot,
|
|
94376
|
+
relativeDirPath,
|
|
94377
|
+
relativeFilePath,
|
|
94378
|
+
frontmatter: {
|
|
94379
|
+
name: "",
|
|
94380
|
+
description: ""
|
|
94381
|
+
},
|
|
94382
|
+
body: "",
|
|
94383
|
+
fileContent: "",
|
|
94384
|
+
validate: false
|
|
94385
|
+
});
|
|
94386
|
+
}
|
|
94387
|
+
};
|
|
94388
|
+
//#endregion
|
|
92672
94389
|
//#region src/features/subagents/zcode-subagent.ts
|
|
92673
94390
|
const ZcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
92674
94391
|
name: zod_mini.z.string(),
|
|
@@ -93134,6 +94851,17 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93134
94851
|
filePattern: "*.md"
|
|
93135
94852
|
}
|
|
93136
94853
|
}],
|
|
94854
|
+
["firebender", {
|
|
94855
|
+
class: FirebenderSubagent,
|
|
94856
|
+
meta: {
|
|
94857
|
+
supportsProject: true,
|
|
94858
|
+
supportsSimulated: false,
|
|
94859
|
+
supportsGlobal: true,
|
|
94860
|
+
emitsEmptyAggregate: true,
|
|
94861
|
+
emitsEmptyAggregateOnDelete: true,
|
|
94862
|
+
filePattern: "*.md"
|
|
94863
|
+
}
|
|
94864
|
+
}],
|
|
93137
94865
|
["goose", {
|
|
93138
94866
|
class: GooseSubagent,
|
|
93139
94867
|
meta: {
|
|
@@ -93361,6 +95089,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93361
95089
|
filePattern: "*.toml"
|
|
93362
95090
|
}
|
|
93363
95091
|
}],
|
|
95092
|
+
["xum", {
|
|
95093
|
+
class: XumSubagent,
|
|
95094
|
+
meta: {
|
|
95095
|
+
supportsProject: true,
|
|
95096
|
+
supportsSimulated: false,
|
|
95097
|
+
supportsGlobal: true,
|
|
95098
|
+
filePattern: "*.md"
|
|
95099
|
+
}
|
|
95100
|
+
}],
|
|
93364
95101
|
["zcode", {
|
|
93365
95102
|
class: ZcodeSubagent,
|
|
93366
95103
|
meta: {
|
|
@@ -93415,6 +95152,7 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
93415
95152
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
93416
95153
|
toolTarget;
|
|
93417
95154
|
global;
|
|
95155
|
+
deleteOrphans;
|
|
93418
95156
|
getFactory;
|
|
93419
95157
|
/**
|
|
93420
95158
|
* Whether the last `loadRulesyncFiles` found a `subagents/` directory under
|
|
@@ -93422,13 +95160,14 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93422
95160
|
* agents in its Pool settings are its own and must not be retracted.
|
|
93423
95161
|
*/
|
|
93424
95162
|
rulesyncSourceDirFound = false;
|
|
93425
|
-
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
95163
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, deleteOrphans = false, logger }) {
|
|
93426
95164
|
super({
|
|
93427
95165
|
outputRoot,
|
|
93428
95166
|
inputRoots,
|
|
93429
95167
|
dryRun,
|
|
93430
95168
|
logger
|
|
93431
95169
|
});
|
|
95170
|
+
this.deleteOrphans = deleteOrphans;
|
|
93432
95171
|
const result = SubagentsProcessorToolTargetSchema.safeParse(toolTarget);
|
|
93433
95172
|
if (!result.success) throw new Error(`Invalid tool target for SubagentsProcessor: ${toolTarget}. ${formatError(result.error)}`);
|
|
93434
95173
|
this.toolTarget = result.data;
|
|
@@ -93436,7 +95175,9 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93436
95175
|
this.getFactory = getFactory;
|
|
93437
95176
|
}
|
|
93438
95177
|
emitsToolFilesForEmptySource() {
|
|
93439
|
-
|
|
95178
|
+
const { meta } = this.getFactory(this.toolTarget);
|
|
95179
|
+
if (meta.emitsEmptyAggregate !== true) return false;
|
|
95180
|
+
return this.rulesyncSourceDirFound || this.deleteOrphans && meta.emitsEmptyAggregateOnDelete === true;
|
|
93440
95181
|
}
|
|
93441
95182
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
93442
95183
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
@@ -93452,11 +95193,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93452
95193
|
});
|
|
93453
95194
|
if (factory.class.fromRulesyncSubagents) {
|
|
93454
95195
|
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
93455
|
-
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
95196
|
+
const toolSubagents = await factory.class.fromRulesyncSubagents({
|
|
93456
95197
|
outputRoot: this.outputRoot,
|
|
93457
95198
|
rulesyncSubagents: targeted,
|
|
93458
95199
|
global: this.global,
|
|
93459
|
-
logger: this.logger
|
|
95200
|
+
logger: this.logger,
|
|
95201
|
+
deleteOrphans: this.deleteOrphans && !this.hasRulesyncSourceLoadFailure()
|
|
93460
95202
|
});
|
|
93461
95203
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
93462
95204
|
}
|
|
@@ -94202,7 +95944,7 @@ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
|
|
|
94202
95944
|
/**
|
|
94203
95945
|
* Globs that match the whole project and so say nothing beyond "always".
|
|
94204
95946
|
*/
|
|
94205
|
-
const UNIVERSAL_GLOBS$
|
|
95947
|
+
const UNIVERSAL_GLOBS$6 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
94206
95948
|
const METADATA_DELIMITER = "---";
|
|
94207
95949
|
/**
|
|
94208
95950
|
* Splits text into lines that keep their own line terminator, so the body can
|
|
@@ -94327,7 +96069,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
|
|
|
94327
96069
|
*/
|
|
94328
96070
|
static buildMetadata(frontmatter) {
|
|
94329
96071
|
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$
|
|
96072
|
+
const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$6.has(glob));
|
|
94331
96073
|
const instructions = frontmatter.description && flattenValue(frontmatter.description);
|
|
94332
96074
|
const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
|
|
94333
96075
|
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 +98723,7 @@ function normalizePatternList(value) {
|
|
|
96981
98723
|
* an always-on rule into a "when any file is in context" rule. Dropped on
|
|
96982
98724
|
* generate and mapped back on import.
|
|
96983
98725
|
*/
|
|
96984
|
-
const UNIVERSAL_GLOBS$
|
|
98726
|
+
const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
96985
98727
|
/**
|
|
96986
98728
|
* Rule generator for Continue (continuedev/continue), the open-source IDE
|
|
96987
98729
|
* extension and CLI (`cn`).
|
|
@@ -97095,7 +98837,7 @@ var ContinueRule = class ContinueRule extends ToolRule {
|
|
|
97095
98837
|
if (alwaysApply === true) return;
|
|
97096
98838
|
const targetGlobs = continueGlobs ?? parentGlobs;
|
|
97097
98839
|
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
97098
|
-
if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$
|
|
98840
|
+
if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$5.has(glob.trim()))) return;
|
|
97099
98841
|
return targetGlobs;
|
|
97100
98842
|
}
|
|
97101
98843
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
@@ -97587,7 +99329,7 @@ const CursorRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
97587
99329
|
* does not already say. Same pair the Cline and Qwen Code adapters treat as
|
|
97588
99330
|
* universal.
|
|
97589
99331
|
*/
|
|
97590
|
-
const UNIVERSAL_GLOBS$
|
|
99332
|
+
const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
97591
99333
|
var CursorRule = class CursorRule extends ToolRule {
|
|
97592
99334
|
frontmatter;
|
|
97593
99335
|
body;
|
|
@@ -97628,7 +99370,8 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
97628
99370
|
}
|
|
97629
99371
|
/**
|
|
97630
99372
|
* 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
|
|
99373
|
+
* MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially.
|
|
99374
|
+
* Also used by Firebender, whose `.firebender/rules/*.mdc` files carry the same frontmatter.
|
|
97632
99375
|
*/
|
|
97633
99376
|
static parseCursorFrontmatter(fileContent, filePath) {
|
|
97634
99377
|
const bounds = findFrontmatterBlockBounds(fileContent);
|
|
@@ -97691,7 +99434,7 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
97691
99434
|
static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
|
|
97692
99435
|
const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
|
|
97693
99436
|
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
97694
|
-
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$
|
|
99437
|
+
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
|
|
97695
99438
|
return targetGlobs.join(",");
|
|
97696
99439
|
}
|
|
97697
99440
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
@@ -98605,6 +100348,211 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
98605
100348
|
}
|
|
98606
100349
|
};
|
|
98607
100350
|
//#endregion
|
|
100351
|
+
//#region src/features/rules/firebender-rule.ts
|
|
100352
|
+
/**
|
|
100353
|
+
* Frontmatter of a Firebender `.mdc` rule. Every field is optional; `globs`
|
|
100354
|
+
* is one comma-separated string, and a YAML list is tolerated on import.
|
|
100355
|
+
*/
|
|
100356
|
+
const FirebenderRuleFrontmatterSchema = zod_mini.z.looseObject({
|
|
100357
|
+
description: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.string())),
|
|
100358
|
+
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())]))),
|
|
100359
|
+
alwaysApply: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.boolean()))
|
|
100360
|
+
});
|
|
100361
|
+
/** Globs that match every file, and so add nothing to an always-applied rule. */
|
|
100362
|
+
const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
100363
|
+
function normalizeGlobs$2(globs) {
|
|
100364
|
+
if (globs === null || globs === void 0) return [];
|
|
100365
|
+
return (typeof globs === "string" ? globs.split(",") : globs).map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
100366
|
+
}
|
|
100367
|
+
/**
|
|
100368
|
+
* Rule generator for Firebender.
|
|
100369
|
+
*
|
|
100370
|
+
* Root rule: Firebender looks for `.firebender/AGENTS.md` first (then the
|
|
100371
|
+
* project-root `AGENTS.md`, then the same pair in parent directories, then
|
|
100372
|
+
* `~/.firebender/AGENTS.md`) and uses the first file it finds, so the root rule
|
|
100373
|
+
* is written to `.firebender/AGENTS.md` in the project and to
|
|
100374
|
+
* `~/.firebender/AGENTS.md` globally, where it does not collide with the
|
|
100375
|
+
* project-root `AGENTS.md` the `agentsmd` target owns.
|
|
100376
|
+
*
|
|
100377
|
+
* Non-root rules: `.firebender/rules/*.mdc` (project) and
|
|
100378
|
+
* `~/.firebender/rules/*.mdc` (personal), each with optional Cursor-style
|
|
100379
|
+
* frontmatter: `description`, `globs` (one comma-separated string) and
|
|
100380
|
+
* `alwaysApply`. A rule with specific globs is written with
|
|
100381
|
+
* `alwaysApply: false` and those globs; every other rule is always applied.
|
|
100382
|
+
*
|
|
100383
|
+
* @see https://docs.firebender.com/api-reference/rules
|
|
100384
|
+
* @see https://docs.firebender.com/api-reference/agents-md
|
|
100385
|
+
*/
|
|
100386
|
+
var FirebenderRule = class FirebenderRule extends ToolRule {
|
|
100387
|
+
frontmatter;
|
|
100388
|
+
body;
|
|
100389
|
+
constructor({ frontmatter = {}, body, ...rest }) {
|
|
100390
|
+
if (rest.validate && !rest.root) {
|
|
100391
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
100392
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
100393
|
+
}
|
|
100394
|
+
const resolvedBody = body ?? rest.fileContent;
|
|
100395
|
+
super({
|
|
100396
|
+
...rest,
|
|
100397
|
+
fileContent: rest.root ? resolvedBody : stringifyFrontmatter(resolvedBody, FirebenderRule.toOutputFrontmatter(frontmatter), { avoidBlockScalars: true })
|
|
100398
|
+
});
|
|
100399
|
+
this.frontmatter = frontmatter;
|
|
100400
|
+
this.body = resolvedBody;
|
|
100401
|
+
}
|
|
100402
|
+
static getSettablePaths({ excludeToolDir } = {}) {
|
|
100403
|
+
return {
|
|
100404
|
+
root: {
|
|
100405
|
+
relativeDirPath: buildToolPath(FIREBENDER_DIR, ".", excludeToolDir),
|
|
100406
|
+
relativeFilePath: FIREBENDER_AGENTS_MD_FILE_NAME
|
|
100407
|
+
},
|
|
100408
|
+
nonRoot: { relativeDirPath: buildToolPath(FIREBENDER_DIR, "rules", excludeToolDir) }
|
|
100409
|
+
};
|
|
100410
|
+
}
|
|
100411
|
+
/**
|
|
100412
|
+
* The frontmatter written to a `.mdc` file: `alwaysApply` first, then the
|
|
100413
|
+
* flattened `description`, then the globs joined the way Firebender's docs
|
|
100414
|
+
* spell them (`"*.kt, *.kts"`).
|
|
100415
|
+
*/
|
|
100416
|
+
static toOutputFrontmatter(frontmatter) {
|
|
100417
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description.replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim() : "";
|
|
100418
|
+
const globs = normalizeGlobs$2(frontmatter.globs);
|
|
100419
|
+
return {
|
|
100420
|
+
...typeof frontmatter.alwaysApply === "boolean" && { alwaysApply: frontmatter.alwaysApply },
|
|
100421
|
+
...description !== "" && { description },
|
|
100422
|
+
...globs.length > 0 && { globs: globs.join(", ") }
|
|
100423
|
+
};
|
|
100424
|
+
}
|
|
100425
|
+
/**
|
|
100426
|
+
* Derives the Firebender frontmatter from the canonical one: specific globs
|
|
100427
|
+
* make a file-scoped rule, everything else (no globs, universal globs) is
|
|
100428
|
+
* always applied. Firebender splits `globs` on every comma, so a brace
|
|
100429
|
+
* alternation such as `*.{ts,tsx}` is expanded into one glob per branch.
|
|
100430
|
+
*/
|
|
100431
|
+
static buildFrontmatter(frontmatter) {
|
|
100432
|
+
const globs = normalizeGlobs$2(frontmatter.globs ?? []).flatMap((glob) => expandBraceAlternations(glob));
|
|
100433
|
+
const specificGlobs = globs.filter((glob) => !UNIVERSAL_GLOBS$3.has(glob));
|
|
100434
|
+
const isFileScoped = specificGlobs.length > 0 && specificGlobs.length === globs.length;
|
|
100435
|
+
const description = frontmatter.description?.trim() || void 0;
|
|
100436
|
+
return {
|
|
100437
|
+
alwaysApply: !isFileScoped,
|
|
100438
|
+
...description !== void 0 && { description },
|
|
100439
|
+
...isFileScoped && { globs: specificGlobs.join(", ") }
|
|
100440
|
+
};
|
|
100441
|
+
}
|
|
100442
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
100443
|
+
const paths = this.getSettablePaths();
|
|
100444
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
100445
|
+
if (rulesyncFrontmatter.root ?? false) return new FirebenderRule({
|
|
100446
|
+
outputRoot,
|
|
100447
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
100448
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
100449
|
+
fileContent: rulesyncRule.getBody(),
|
|
100450
|
+
validate,
|
|
100451
|
+
root: true,
|
|
100452
|
+
description: rulesyncFrontmatter.description,
|
|
100453
|
+
globs: rulesyncFrontmatter.globs
|
|
100454
|
+
});
|
|
100455
|
+
return new FirebenderRule({
|
|
100456
|
+
outputRoot,
|
|
100457
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
100458
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath().replace(/\.md$/, ".mdc"),
|
|
100459
|
+
frontmatter: this.buildFrontmatter(rulesyncFrontmatter),
|
|
100460
|
+
body: rulesyncRule.getBody(),
|
|
100461
|
+
fileContent: "",
|
|
100462
|
+
validate,
|
|
100463
|
+
root: false
|
|
100464
|
+
});
|
|
100465
|
+
}
|
|
100466
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
100467
|
+
const paths = this.getSettablePaths();
|
|
100468
|
+
if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
|
|
100469
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.root.relativeDirPath, paths.root.relativeFilePath));
|
|
100470
|
+
return new FirebenderRule({
|
|
100471
|
+
outputRoot,
|
|
100472
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
100473
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
100474
|
+
fileContent,
|
|
100475
|
+
validate,
|
|
100476
|
+
root: true
|
|
100477
|
+
});
|
|
100478
|
+
}
|
|
100479
|
+
const filePath = (0, node_path.join)(outputRoot, paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
100480
|
+
const fileContent = await readFileContent(filePath);
|
|
100481
|
+
const { frontmatter, body } = CursorRule.parseCursorFrontmatter(fileContent, filePath);
|
|
100482
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
100483
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
100484
|
+
return new FirebenderRule({
|
|
100485
|
+
outputRoot,
|
|
100486
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
100487
|
+
relativeFilePath,
|
|
100488
|
+
frontmatter: result.data,
|
|
100489
|
+
body: body.trim(),
|
|
100490
|
+
fileContent: "",
|
|
100491
|
+
validate,
|
|
100492
|
+
root: false
|
|
100493
|
+
});
|
|
100494
|
+
}
|
|
100495
|
+
toRulesyncRule() {
|
|
100496
|
+
if (this.isRoot()) return this.toRulesyncRuleDefault();
|
|
100497
|
+
const sourceGlobs = normalizeGlobs$2(this.frontmatter.globs);
|
|
100498
|
+
const alwaysApply = this.frontmatter.alwaysApply ?? void 0;
|
|
100499
|
+
const description = this.frontmatter.description?.trim() || void 0;
|
|
100500
|
+
const globs = alwaysApply === true || sourceGlobs.length === 0 ? ["**/*"] : sourceGlobs;
|
|
100501
|
+
return new RulesyncRule({
|
|
100502
|
+
outputRoot: process.cwd(),
|
|
100503
|
+
frontmatter: {
|
|
100504
|
+
targets: ["*"],
|
|
100505
|
+
root: false,
|
|
100506
|
+
...description !== void 0 && { description },
|
|
100507
|
+
globs
|
|
100508
|
+
},
|
|
100509
|
+
body: this.body,
|
|
100510
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
100511
|
+
relativeFilePath: this.getRelativeFilePath().replace(/\.mdc$/, ".md"),
|
|
100512
|
+
validate: true
|
|
100513
|
+
});
|
|
100514
|
+
}
|
|
100515
|
+
validate() {
|
|
100516
|
+
if (this.isRoot() || !this.frontmatter) return {
|
|
100517
|
+
success: true,
|
|
100518
|
+
error: null
|
|
100519
|
+
};
|
|
100520
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
100521
|
+
if (result.success) return {
|
|
100522
|
+
success: true,
|
|
100523
|
+
error: null
|
|
100524
|
+
};
|
|
100525
|
+
return {
|
|
100526
|
+
success: false,
|
|
100527
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
100528
|
+
};
|
|
100529
|
+
}
|
|
100530
|
+
getFrontmatter() {
|
|
100531
|
+
return this.frontmatter;
|
|
100532
|
+
}
|
|
100533
|
+
getBody() {
|
|
100534
|
+
return this.body;
|
|
100535
|
+
}
|
|
100536
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
100537
|
+
const paths = this.getSettablePaths();
|
|
100538
|
+
const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
100539
|
+
return new FirebenderRule({
|
|
100540
|
+
outputRoot,
|
|
100541
|
+
relativeDirPath,
|
|
100542
|
+
relativeFilePath,
|
|
100543
|
+
fileContent: "",
|
|
100544
|
+
validate: false,
|
|
100545
|
+
root: isRoot
|
|
100546
|
+
});
|
|
100547
|
+
}
|
|
100548
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
100549
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
100550
|
+
rulesyncRule,
|
|
100551
|
+
toolTarget: "firebender"
|
|
100552
|
+
});
|
|
100553
|
+
}
|
|
100554
|
+
};
|
|
100555
|
+
//#endregion
|
|
98608
100556
|
//#region src/features/rules/gitlabduo-rule.ts
|
|
98609
100557
|
/**
|
|
98610
100558
|
* GitLab Duo CLI reads custom rules from a single plain-Markdown file:
|
|
@@ -101886,6 +103834,117 @@ var WarpRule = class extends NestedAgentsmdRule {
|
|
|
101886
103834
|
}
|
|
101887
103835
|
};
|
|
101888
103836
|
//#endregion
|
|
103837
|
+
//#region src/features/rules/xum-rule.ts
|
|
103838
|
+
/** An ATX level-1 heading line (`# Title`), which Xum's parser treats as an H1. */
|
|
103839
|
+
const ATX_H1_LINE = /^ {0,3}#(?:[ \t]|$)/;
|
|
103840
|
+
/**
|
|
103841
|
+
* Make `body` start at a level-1 heading, adding `# <heading>` when it does not
|
|
103842
|
+
* already open with one.
|
|
103843
|
+
*
|
|
103844
|
+
* Xum reads `.xum/AGENTS.md` (and appends `AGENTS.local.md`) as one Markdown
|
|
103845
|
+
* document and treats a `Mode:` / `Model:` / `Tool:` heading as a scoped
|
|
103846
|
+
* section that runs until the next heading of the same or a higher level.
|
|
103847
|
+
* Fragments rulesync concatenates into that document — folded topic rules and
|
|
103848
|
+
* the local overlay — would otherwise fall inside a scoped section the
|
|
103849
|
+
* preceding fragment ends with, and Xum would inject them only in that mode.
|
|
103850
|
+
* Only an H1 closes a section of any level, so each such fragment opens with
|
|
103851
|
+
* one.
|
|
103852
|
+
*
|
|
103853
|
+
* @see https://github.com/coder/xum/blob/main/src/node/utils/main/markdown.ts
|
|
103854
|
+
*/
|
|
103855
|
+
function ensureXumSectionBoundary({ body, heading }) {
|
|
103856
|
+
const firstLine = body.split(/\r?\n/).find((line) => line.trim() !== "");
|
|
103857
|
+
if (firstLine !== void 0 && ATX_H1_LINE.test(firstLine)) return body;
|
|
103858
|
+
const safeHeading = heading.replaceAll(/[\r\n:]/g, " ").trim() || "Instructions";
|
|
103859
|
+
return body.trim() === "" ? `# ${safeHeading}\n` : `# ${safeHeading}\n\n${body}`;
|
|
103860
|
+
}
|
|
103861
|
+
/** The heading a folded rule opens with: its file name without the extension. */
|
|
103862
|
+
function xumRuleHeading(rulesyncRule) {
|
|
103863
|
+
const fileName = (0, node_path.basename)(rulesyncRule.getRelativeFilePath());
|
|
103864
|
+
return fileName.slice(0, fileName.length - (0, node_path.extname)(fileName).length);
|
|
103865
|
+
}
|
|
103866
|
+
/**
|
|
103867
|
+
* Rule generator for Xum (Coder's parallel coding-agent app, formerly Mux).
|
|
103868
|
+
*
|
|
103869
|
+
* Xum layers its own `.xum/AGENTS.md` (project) and `~/.xum/AGENTS.md` (user)
|
|
103870
|
+
* on top of the shared root `AGENTS.md` / `AGENT.md` / `CLAUDE.md`, which the
|
|
103871
|
+
* `agentsmd` target already writes, so this target writes only the
|
|
103872
|
+
* Xum-dedicated file. Only these files honor the `## Model: <regex>` and
|
|
103873
|
+
* `## Mode: <mode>` scoped sections; rulesync passes the body through as-is,
|
|
103874
|
+
* so such sections authored in a rule keep working. The personal
|
|
103875
|
+
* `.xum/AGENTS.local.md` overlay is written for a `localRoot: true` rule.
|
|
103876
|
+
*
|
|
103877
|
+
* @see https://xum.cdr.dev/agents/instruction-files
|
|
103878
|
+
*/
|
|
103879
|
+
var XumRule = class XumRule extends ToolRule {
|
|
103880
|
+
constructor({ fileContent, root, ...rest }) {
|
|
103881
|
+
super({
|
|
103882
|
+
...rest,
|
|
103883
|
+
fileContent,
|
|
103884
|
+
root: root ?? false
|
|
103885
|
+
});
|
|
103886
|
+
}
|
|
103887
|
+
static getSettablePaths(_options = {}) {
|
|
103888
|
+
return { root: {
|
|
103889
|
+
relativeDirPath: XUM_DIR,
|
|
103890
|
+
relativeFilePath: XUM_RULE_FILE_NAME
|
|
103891
|
+
} };
|
|
103892
|
+
}
|
|
103893
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
103894
|
+
const { root } = this.getSettablePaths({ global });
|
|
103895
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
103896
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
103897
|
+
return new XumRule({
|
|
103898
|
+
outputRoot,
|
|
103899
|
+
relativeDirPath: root.relativeDirPath,
|
|
103900
|
+
relativeFilePath: root.relativeFilePath,
|
|
103901
|
+
fileContent,
|
|
103902
|
+
validate,
|
|
103903
|
+
root: true
|
|
103904
|
+
});
|
|
103905
|
+
}
|
|
103906
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
103907
|
+
const { root } = this.getSettablePaths({ global });
|
|
103908
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
103909
|
+
return new XumRule({
|
|
103910
|
+
outputRoot,
|
|
103911
|
+
relativeDirPath: root.relativeDirPath,
|
|
103912
|
+
relativeFilePath: root.relativeFilePath,
|
|
103913
|
+
fileContent: isRoot ? rulesyncRule.getBody() : ensureXumSectionBoundary({
|
|
103914
|
+
body: rulesyncRule.getBody(),
|
|
103915
|
+
heading: xumRuleHeading(rulesyncRule)
|
|
103916
|
+
}),
|
|
103917
|
+
validate,
|
|
103918
|
+
root: isRoot
|
|
103919
|
+
});
|
|
103920
|
+
}
|
|
103921
|
+
toRulesyncRule() {
|
|
103922
|
+
return this.toRulesyncRuleDefault();
|
|
103923
|
+
}
|
|
103924
|
+
validate() {
|
|
103925
|
+
return {
|
|
103926
|
+
success: true,
|
|
103927
|
+
error: null
|
|
103928
|
+
};
|
|
103929
|
+
}
|
|
103930
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
103931
|
+
return new XumRule({
|
|
103932
|
+
outputRoot,
|
|
103933
|
+
relativeDirPath,
|
|
103934
|
+
relativeFilePath,
|
|
103935
|
+
fileContent: "",
|
|
103936
|
+
validate: false,
|
|
103937
|
+
root: relativeFilePath === "AGENTS.md" && relativeDirPath === ".xum"
|
|
103938
|
+
});
|
|
103939
|
+
}
|
|
103940
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
103941
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
103942
|
+
rulesyncRule,
|
|
103943
|
+
toolTarget: "xum"
|
|
103944
|
+
});
|
|
103945
|
+
}
|
|
103946
|
+
};
|
|
103947
|
+
//#endregion
|
|
101889
103948
|
//#region src/features/rules/zcode-rule.ts
|
|
101890
103949
|
var ZcodeRule = class ZcodeRule extends ToolRule {
|
|
101891
103950
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -102374,6 +104433,14 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
102374
104433
|
collisionPolicy: "compose"
|
|
102375
104434
|
}
|
|
102376
104435
|
}],
|
|
104436
|
+
["firebender", {
|
|
104437
|
+
class: FirebenderRule,
|
|
104438
|
+
meta: {
|
|
104439
|
+
extension: "mdc",
|
|
104440
|
+
supportsGlobal: true,
|
|
104441
|
+
ruleDiscoveryMode: "auto"
|
|
104442
|
+
}
|
|
104443
|
+
}],
|
|
102377
104444
|
["goose", {
|
|
102378
104445
|
class: GooseRule,
|
|
102379
104446
|
meta: {
|
|
@@ -102657,6 +104724,17 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
102657
104724
|
collisionPolicy: "fold"
|
|
102658
104725
|
}
|
|
102659
104726
|
}],
|
|
104727
|
+
["xum", {
|
|
104728
|
+
class: XumRule,
|
|
104729
|
+
meta: {
|
|
104730
|
+
extension: "md",
|
|
104731
|
+
supportsGlobal: true,
|
|
104732
|
+
ruleDiscoveryMode: "auto",
|
|
104733
|
+
collisionPolicy: "fold",
|
|
104734
|
+
localRootMode: "separate-local-file",
|
|
104735
|
+
localRootFileName: XUM_LOCAL_RULE_FILE_NAME
|
|
104736
|
+
}
|
|
104737
|
+
}],
|
|
102660
104738
|
["devin", {
|
|
102661
104739
|
class: DevinRule,
|
|
102662
104740
|
meta: {
|
|
@@ -103427,18 +105505,39 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
103427
105505
|
root: true,
|
|
103428
105506
|
localRoot
|
|
103429
105507
|
});
|
|
103430
|
-
|
|
103431
|
-
|
|
103432
|
-
|
|
103433
|
-
|
|
103434
|
-
|
|
103435
|
-
relativeDirPath: relativeDirPath ?? ".qwen",
|
|
103436
|
-
relativeFilePath: fileName,
|
|
103437
|
-
fileContent: body,
|
|
103438
|
-
validate: true,
|
|
103439
|
-
root: true,
|
|
105508
|
+
return this.buildToolDirLocalRootFile({
|
|
105509
|
+
factory,
|
|
105510
|
+
fileName,
|
|
105511
|
+
body,
|
|
105512
|
+
relativeDirPath,
|
|
103440
105513
|
localRoot
|
|
103441
105514
|
});
|
|
105515
|
+
}
|
|
105516
|
+
/**
|
|
105517
|
+
* Qwen Code and Xum read the personal local file from their tool directory
|
|
105518
|
+
* (`.qwen/QWEN.local.md`, `.xum/AGENTS.local.md`), not the project root
|
|
105519
|
+
* (project scope only; global handling never reaches here).
|
|
105520
|
+
*/
|
|
105521
|
+
buildToolDirLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot }) {
|
|
105522
|
+
const toolDirEntry = [[QwencodeRule, QWENCODE_DIR], [XumRule, XUM_DIR]].find(([base]) => isClassOrSubclassOf({
|
|
105523
|
+
candidate: factory.class,
|
|
105524
|
+
base
|
|
105525
|
+
}));
|
|
105526
|
+
if (toolDirEntry) {
|
|
105527
|
+
const [toolDirClass, toolDir] = toolDirEntry;
|
|
105528
|
+
return new toolDirClass({
|
|
105529
|
+
outputRoot: this.outputRoot,
|
|
105530
|
+
relativeDirPath: relativeDirPath ?? toolDir,
|
|
105531
|
+
relativeFilePath: fileName,
|
|
105532
|
+
fileContent: toolDirClass === XumRule && !localRoot ? ensureXumSectionBoundary({
|
|
105533
|
+
body,
|
|
105534
|
+
heading: "Local instructions"
|
|
105535
|
+
}) : body,
|
|
105536
|
+
validate: true,
|
|
105537
|
+
root: true,
|
|
105538
|
+
localRoot
|
|
105539
|
+
});
|
|
105540
|
+
}
|
|
103442
105541
|
return null;
|
|
103443
105542
|
}
|
|
103444
105543
|
/**
|
|
@@ -106190,6 +108289,7 @@ async function generateSubagentsCore(params) {
|
|
|
106190
108289
|
toolTarget,
|
|
106191
108290
|
global: config.getGlobal(),
|
|
106192
108291
|
dryRun: config.isPreviewMode(),
|
|
108292
|
+
deleteOrphans: config.getDelete(),
|
|
106193
108293
|
logger
|
|
106194
108294
|
});
|
|
106195
108295
|
const result = await processFeatureWithRulesyncFiles({
|