rulesync 16.21.0 → 16.22.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 +2 -0
- package/dist/cli/index.cjs +118 -35
- package/dist/cli/index.js +118 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-5_n-Y8N6.js → import-BKqbq4Ut.js} +2098 -665
- package/dist/import-BKqbq4Ut.js.map +1 -0
- package/dist/{import-7UmDF35I.cjs → import-CUyeYGxZ.cjs} +2131 -680
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +37 -19
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +37 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-5_n-Y8N6.js.map +0 -1
|
@@ -246,6 +246,35 @@ function hasDeceptiveHiddenCharacters(text) {
|
|
|
246
246
|
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
|
+
/** A mark that draws around the character before it — a circle, a square, a keycap box. */
|
|
250
|
+
const ENCLOSING_MARK_PATTERN = /\p{Me}/u;
|
|
251
|
+
/**
|
|
252
|
+
* Whether `text` carries an enclosing mark that is not the keycap of an emoji
|
|
253
|
+
* keycap sequence.
|
|
254
|
+
*
|
|
255
|
+
* An enclosing mark (`\p{Me}`: U+20DD COMBINING ENCLOSING CIRCLE, U+20E3
|
|
256
|
+
* COMBINING ENCLOSING KEYCAP, the Cyrillic and Vedic ones) is drawn over the
|
|
257
|
+
* character before it and takes no column of its own, so `pdf` with one after
|
|
258
|
+
* it occupies the three columns of `pdf` and is a fourth directory underneath.
|
|
259
|
+
* Unlike a joiner or a variation selector it is not invisible — the box is
|
|
260
|
+
* drawn — which is why `hasDeceptiveHiddenCharacters` does not refuse it and
|
|
261
|
+
* why it is a question for the confusable-name note instead: the row is drawn,
|
|
262
|
+
* only not the way its name reads. The one place an enclosing mark belongs in
|
|
263
|
+
* a name is the keycap sequence of UTS #51, which `isKeycapSequence` matches
|
|
264
|
+
* whole; every other one is left over.
|
|
265
|
+
*
|
|
266
|
+
* Restricted to `\p{Me}` on purpose: a non-spacing mark (`\p{Mn}`) is how
|
|
267
|
+
* Devanagari, Arabic and Vietnamese write, and folding those would mark
|
|
268
|
+
* ordinary names in every one of them.
|
|
269
|
+
*/
|
|
270
|
+
function hasEnclosingMarkOutsideKeycap(text) {
|
|
271
|
+
const characters = [...text];
|
|
272
|
+
return characters.some((character, index) => ENCLOSING_MARK_PATTERN.test(character) && !isKeycapSequence({
|
|
273
|
+
base: characters[index - 2],
|
|
274
|
+
selector: characters[index - 1] ?? "",
|
|
275
|
+
following: character
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
249
278
|
//#endregion
|
|
250
279
|
//#region src/utils/truncate.ts
|
|
251
280
|
/**
|
|
@@ -415,34 +444,34 @@ const isFeatureValueEnabled = (value) => {
|
|
|
415
444
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
416
445
|
//#endregion
|
|
417
446
|
//#region src/constants/rulesync-paths.ts
|
|
418
|
-
const { join: join$
|
|
447
|
+
const { join: join$304 } = node_path.posix;
|
|
419
448
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
420
449
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
421
450
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
422
451
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
423
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
452
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$304(RULES_FEATURE_SUBDIR, ".curated");
|
|
424
453
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
425
454
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
426
455
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
427
456
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
428
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
429
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
430
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
431
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
432
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
433
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
434
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
435
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
436
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
437
|
-
join$
|
|
438
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
439
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
457
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$304(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
458
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
459
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
460
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
461
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
462
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
463
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
464
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
465
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
466
|
+
join$304(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
467
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
468
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
440
469
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
441
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
470
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
442
471
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
443
472
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
444
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
445
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
473
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
446
475
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
447
476
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
448
477
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -469,9 +498,11 @@ const rulesProcessorToolTargetTuple = [
|
|
|
469
498
|
"claudecode",
|
|
470
499
|
"claudecode-legacy",
|
|
471
500
|
"cline",
|
|
501
|
+
"codebuddy",
|
|
472
502
|
"codexcli",
|
|
473
503
|
"copilot",
|
|
474
504
|
"copilotcli",
|
|
505
|
+
"crush",
|
|
475
506
|
"cursor",
|
|
476
507
|
"deepagents",
|
|
477
508
|
"factorydroid",
|
|
@@ -507,6 +538,7 @@ const ignoreProcessorToolTargetTuple = [
|
|
|
507
538
|
"claudecode",
|
|
508
539
|
"claudecode-legacy",
|
|
509
540
|
"cline",
|
|
541
|
+
"crush",
|
|
510
542
|
"cursor",
|
|
511
543
|
"hermesagent",
|
|
512
544
|
"junie",
|
|
@@ -648,6 +680,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
648
680
|
"codexcli",
|
|
649
681
|
"copilot",
|
|
650
682
|
"copilotcli",
|
|
683
|
+
"crush",
|
|
651
684
|
"cursor",
|
|
652
685
|
"deepagents",
|
|
653
686
|
"factorydroid",
|
|
@@ -3352,6 +3385,42 @@ var RulesyncFile = class extends AiFile {
|
|
|
3352
3385
|
}
|
|
3353
3386
|
};
|
|
3354
3387
|
//#endregion
|
|
3388
|
+
//#region src/utils/prototype-pollution.ts
|
|
3389
|
+
/**
|
|
3390
|
+
* Keys that, if walked into when constructing or merging objects from
|
|
3391
|
+
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
3392
|
+
* chain) and propagate state to every other object in the runtime. Any code
|
|
3393
|
+
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
3394
|
+
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
3395
|
+
*/
|
|
3396
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
3397
|
+
"__proto__",
|
|
3398
|
+
"constructor",
|
|
3399
|
+
"prototype"
|
|
3400
|
+
]);
|
|
3401
|
+
function isPrototypePollutionKey(key) {
|
|
3402
|
+
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Returns a shallow copy of a record's own entries with every
|
|
3406
|
+
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
3407
|
+
*
|
|
3408
|
+
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
3409
|
+
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
3410
|
+
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
3411
|
+
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
3412
|
+
* prototype). Walking the entries through this helper severs that path while
|
|
3413
|
+
* preserving every legitimate key.
|
|
3414
|
+
*/
|
|
3415
|
+
function omitPrototypePollutionKeys(record) {
|
|
3416
|
+
const sanitized = {};
|
|
3417
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3418
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3419
|
+
sanitized[key] = value;
|
|
3420
|
+
}
|
|
3421
|
+
return sanitized;
|
|
3422
|
+
}
|
|
3423
|
+
//#endregion
|
|
3355
3424
|
//#region src/utils/type-guards.ts
|
|
3356
3425
|
/**
|
|
3357
3426
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
@@ -3416,67 +3485,192 @@ function loadYaml(content) {
|
|
|
3416
3485
|
}
|
|
3417
3486
|
//#endregion
|
|
3418
3487
|
//#region src/utils/frontmatter.ts
|
|
3419
|
-
|
|
3488
|
+
/**
|
|
3489
|
+
* Upper bound on the number of values a frontmatter document may expand to
|
|
3490
|
+
* once every YAML alias is written out.
|
|
3491
|
+
*
|
|
3492
|
+
* A YAML alias makes one parsed container reachable from many keys, and the
|
|
3493
|
+
* cleaners below copy each reachable value, so a small file with a few levels
|
|
3494
|
+
* of nested aliases (an "alias bomb") can expand into megabytes of output or
|
|
3495
|
+
* exhaust the heap. Counting every visited value against this budget turns
|
|
3496
|
+
* that into an error instead. Real frontmatter is a handful of keys; even a
|
|
3497
|
+
* generous skill manifest stays orders of magnitude below the limit.
|
|
3498
|
+
*/
|
|
3499
|
+
const MAX_FRONTMATTER_VALUES = 1e5;
|
|
3500
|
+
/**
|
|
3501
|
+
* Upper bound on the total character count of string leaves a frontmatter
|
|
3502
|
+
* document may expand to.
|
|
3503
|
+
*
|
|
3504
|
+
* {@link MAX_FRONTMATTER_VALUES} bounds how many values are visited, but a
|
|
3505
|
+
* single long string aliased thousands of times still fits that budget while
|
|
3506
|
+
* the duplicated output balloons: one scalar of a few KB, chained through a
|
|
3507
|
+
* handful of aliases within the value budget, can multiply into a document
|
|
3508
|
+
* many megabytes larger than it started. Charging every visited string's
|
|
3509
|
+
* length against this separate budget bounds that output regardless of how
|
|
3510
|
+
* many aliases point at it.
|
|
3511
|
+
*/
|
|
3512
|
+
const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
3513
|
+
/**
|
|
3514
|
+
* Upper bound on the raw character length of the `---`-delimited frontmatter
|
|
3515
|
+
* block itself, checked before it is ever handed to the YAML parser.
|
|
3516
|
+
*
|
|
3517
|
+
* The budgets above only bound the *parsed* document — the walk over
|
|
3518
|
+
* `matter()`'s output — but a complex YAML key (an array or mapping used as a
|
|
3519
|
+
* mapping key) is joined into a string by js-yaml while it parses, and a
|
|
3520
|
+
* mapping with many such keys can cost real memory before that walk ever
|
|
3521
|
+
* starts, or even before `matter()` returns. Capping the raw block size keeps
|
|
3522
|
+
* that parse-time cost bounded regardless of what the block contains. Real
|
|
3523
|
+
* frontmatter blocks are a few hundred bytes at most; even a large project
|
|
3524
|
+
* manifest stays well under this.
|
|
3525
|
+
*/
|
|
3526
|
+
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3527
|
+
/** Charge string content (a string leaf or an object key) against the character budget. */
|
|
3528
|
+
function chargeStringChars({ options, chars }) {
|
|
3529
|
+
options.budget.stringCharsRemaining -= chars;
|
|
3530
|
+
if (options.budget.stringCharsRemaining < 0) throw new Error(`Frontmatter's string values expand to more than ${MAX_FRONTMATTER_STRING_CHARS} characters; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3531
|
+
}
|
|
3532
|
+
function consumeBudget({ options, stringChars = 0 }) {
|
|
3533
|
+
options.budget.remaining -= 1;
|
|
3534
|
+
if (options.budget.remaining < 0) throw new Error(`Frontmatter expands to more than ${MAX_FRONTMATTER_VALUES} values; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3535
|
+
chargeStringChars({
|
|
3536
|
+
options,
|
|
3537
|
+
chars: stringChars
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
/** Enter one more container level, throwing if the depth cap is exceeded. */
|
|
3541
|
+
function enterContainer({ options, container }) {
|
|
3542
|
+
options.depth += 1;
|
|
3543
|
+
if (options.depth > 64) throw new Error(`Frontmatter nests more than 64 levels deep; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3544
|
+
options.ancestors.add(container);
|
|
3545
|
+
}
|
|
3546
|
+
/** Leave a container level entered via {@link enterContainer}. */
|
|
3547
|
+
function leaveContainer({ options, container }) {
|
|
3548
|
+
options.ancestors.delete(container);
|
|
3549
|
+
options.depth -= 1;
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
3552
|
+
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3553
|
+
* string leaf is charged by its own length instead).
|
|
3554
|
+
*
|
|
3555
|
+
* js-yaml's default schema resolves `!!binary` scalars to a `Uint8Array`, and
|
|
3556
|
+
* its dumper writes one back out as base64 — roughly 4 output characters per
|
|
3557
|
+
* 3 input bytes. Without this, an aliased binary blob would walk the budget
|
|
3558
|
+
* for free even though it can dominate the emitted document's size.
|
|
3559
|
+
*/
|
|
3560
|
+
function estimateLeafChars(value) {
|
|
3561
|
+
if (value instanceof Uint8Array) return Math.ceil(value.byteLength / 3) * 4;
|
|
3562
|
+
return 0;
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Copy one parsed value, dropping nullish leaves and cyclic references.
|
|
3566
|
+
*
|
|
3567
|
+
* Every alias is still written out as an independent copy, as gray-matter's
|
|
3568
|
+
* default YAML engine would otherwise serialize shared references as `&ref_0`
|
|
3569
|
+
* anchors that simplified frontmatter parsers cannot read; the expansion is
|
|
3570
|
+
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3571
|
+
*/
|
|
3572
|
+
function deepCleanValue(value, options) {
|
|
3573
|
+
consumeBudget({
|
|
3574
|
+
options,
|
|
3575
|
+
stringChars: typeof value === "string" ? value.length : estimateLeafChars(value)
|
|
3576
|
+
});
|
|
3420
3577
|
if (value === null || value === void 0) return;
|
|
3421
|
-
if (
|
|
3422
|
-
if (
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3578
|
+
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3579
|
+
if (Array.isArray(value)) {
|
|
3580
|
+
if (options.ancestors.has(value)) return;
|
|
3581
|
+
enterContainer({
|
|
3582
|
+
options,
|
|
3583
|
+
container: value
|
|
3584
|
+
});
|
|
3585
|
+
const cleanedArray = [];
|
|
3586
|
+
for (const item of value) {
|
|
3587
|
+
const cleaned = deepCleanValue(item, options);
|
|
3588
|
+
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3427
3589
|
}
|
|
3590
|
+
leaveContainer({
|
|
3591
|
+
options,
|
|
3592
|
+
container: value
|
|
3593
|
+
});
|
|
3594
|
+
return cleanedArray;
|
|
3595
|
+
}
|
|
3596
|
+
if (isPlainObject$1(value)) {
|
|
3597
|
+
if (options.ancestors.has(value)) return;
|
|
3598
|
+
enterContainer({
|
|
3599
|
+
options,
|
|
3600
|
+
container: value
|
|
3601
|
+
});
|
|
3602
|
+
const result = cleanOwnEntries(value, options);
|
|
3603
|
+
leaveContainer({
|
|
3604
|
+
options,
|
|
3605
|
+
container: value
|
|
3606
|
+
});
|
|
3428
3607
|
return result;
|
|
3429
3608
|
}
|
|
3430
3609
|
return value;
|
|
3431
3610
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3611
|
+
/**
|
|
3612
|
+
* Copy the cleaned own entries of a parsed object into a fresh record.
|
|
3613
|
+
*
|
|
3614
|
+
* A YAML parser defines a `__proto__:` key as an own property, and assigning
|
|
3615
|
+
* it back with bracket notation would instead replace the new record's
|
|
3616
|
+
* prototype, whose members zod's loose object schemas then promote to real
|
|
3617
|
+
* keys. So a fetched skill could hide `allowed-tools` under an innocuous
|
|
3618
|
+
* looking `__proto__:` block. That key, `constructor` and `prototype` are
|
|
3619
|
+
* therefore dropped rather than copied, and cannot be used as frontmatter
|
|
3620
|
+
* keys.
|
|
3621
|
+
*/
|
|
3622
|
+
function cleanOwnEntries(obj, options) {
|
|
3434
3623
|
const result = {};
|
|
3435
3624
|
for (const [key, val] of Object.entries(obj)) {
|
|
3436
|
-
|
|
3625
|
+
chargeStringChars({
|
|
3626
|
+
options,
|
|
3627
|
+
chars: key.length
|
|
3628
|
+
});
|
|
3629
|
+
const cleaned = deepCleanValue(val, options);
|
|
3630
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
3437
3631
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
3438
3632
|
}
|
|
3439
3633
|
return result;
|
|
3440
3634
|
}
|
|
3441
|
-
function
|
|
3442
|
-
if (
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3635
|
+
function deepCleanObject(obj, options) {
|
|
3636
|
+
if (!obj || typeof obj !== "object") return {};
|
|
3637
|
+
return cleanOwnEntries(obj, {
|
|
3638
|
+
...options,
|
|
3639
|
+
ancestors: new WeakSet([obj]),
|
|
3640
|
+
budget: {
|
|
3641
|
+
remaining: MAX_FRONTMATTER_VALUES,
|
|
3642
|
+
stringCharsRemaining: MAX_FRONTMATTER_STRING_CHARS
|
|
3643
|
+
},
|
|
3644
|
+
depth: 1
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
/** Drop null and undefined values, recursively. */
|
|
3648
|
+
function deepRemoveNullishObject(obj) {
|
|
3649
|
+
return deepCleanObject(obj, {});
|
|
3454
3650
|
}
|
|
3651
|
+
/** Drop nullish values and collapse every string onto a single line. */
|
|
3455
3652
|
function deepFlattenStringsObject(obj) {
|
|
3456
|
-
|
|
3457
|
-
const result = {};
|
|
3458
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
3459
|
-
const cleaned = deepFlattenStringsValue(val);
|
|
3460
|
-
if (cleaned !== void 0) result[key] = cleaned;
|
|
3461
|
-
}
|
|
3462
|
-
return result;
|
|
3653
|
+
return deepCleanObject(obj, { transformString: (value) => value.replace(/\n+/g, " ").trim() });
|
|
3463
3654
|
}
|
|
3464
3655
|
function stringifyFrontmatter(body, frontmatter, options) {
|
|
3465
3656
|
const { avoidBlockScalars = false } = options ?? {};
|
|
3466
3657
|
const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
|
|
3467
|
-
|
|
3658
|
+
const file = { content: body };
|
|
3659
|
+
if (avoidBlockScalars) return gray_matter.default.stringify(file, cleanFrontmatter, { engines: { yaml: {
|
|
3468
3660
|
parse: (input) => loadYaml(input) ?? {},
|
|
3469
3661
|
stringify: (data) => (0, js_yaml.dump)(data, { lineWidth: -1 })
|
|
3470
3662
|
} } });
|
|
3471
|
-
return gray_matter.default.stringify(
|
|
3663
|
+
return gray_matter.default.stringify(file, cleanFrontmatter);
|
|
3472
3664
|
}
|
|
3473
3665
|
function parseFrontmatter(content, filePath) {
|
|
3474
3666
|
let frontmatter;
|
|
3475
3667
|
let body;
|
|
3476
3668
|
let hasFrontmatter;
|
|
3477
3669
|
try {
|
|
3670
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3671
|
+
if (bounds && bounds.blockEnd - bounds.blockStart > 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)`);
|
|
3478
3672
|
const result = (0, gray_matter.default)(content, {});
|
|
3479
|
-
frontmatter = result.data;
|
|
3673
|
+
frontmatter = deepRemoveNullishObject(result.data);
|
|
3480
3674
|
body = result.content;
|
|
3481
3675
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
3482
3676
|
} catch (error) {
|
|
@@ -3484,7 +3678,7 @@ function parseFrontmatter(content, filePath) {
|
|
|
3484
3678
|
throw error;
|
|
3485
3679
|
}
|
|
3486
3680
|
return {
|
|
3487
|
-
frontmatter
|
|
3681
|
+
frontmatter,
|
|
3488
3682
|
body,
|
|
3489
3683
|
hasFrontmatter
|
|
3490
3684
|
};
|
|
@@ -3533,17 +3727,34 @@ function repairFrontmatterLine(line) {
|
|
|
3533
3727
|
};
|
|
3534
3728
|
}
|
|
3535
3729
|
/**
|
|
3536
|
-
*
|
|
3537
|
-
*
|
|
3538
|
-
*
|
|
3730
|
+
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
3731
|
+
* without parsing it. Shared by the size guard in {@link parseFrontmatter} and
|
|
3732
|
+
* the YAML repair pass below, so both agree on exactly what gray-matter would
|
|
3733
|
+
* treat as the block: gray-matter ends it at the first `\n---`, with no
|
|
3734
|
+
* requirement that the delimiter be alone on its line, so a stricter pattern
|
|
3735
|
+
* here would run past gray-matter's delimiter and act on text that is really
|
|
3736
|
+
* the body.
|
|
3539
3737
|
*/
|
|
3540
|
-
function
|
|
3738
|
+
function findFrontmatterBlockBounds(content) {
|
|
3541
3739
|
const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
|
|
3542
3740
|
if (!opening) return;
|
|
3543
3741
|
const blockStart = opening[0].length;
|
|
3544
3742
|
const closing = /\r?\n---/.exec(content.slice(blockStart));
|
|
3545
3743
|
if (!closing) return;
|
|
3546
|
-
|
|
3744
|
+
return {
|
|
3745
|
+
blockStart,
|
|
3746
|
+
blockEnd: blockStart + closing.index
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* Quote the unquoted scalars that make a frontmatter block unparseable, or
|
|
3751
|
+
* return `undefined` when there is nothing to repair. Only the frontmatter
|
|
3752
|
+
* block is rewritten; the body is passed through untouched.
|
|
3753
|
+
*/
|
|
3754
|
+
function repairMalformedFrontmatterYaml(content) {
|
|
3755
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3756
|
+
if (!bounds) return;
|
|
3757
|
+
const { blockStart, blockEnd } = bounds;
|
|
3547
3758
|
const block = content.slice(blockStart, blockEnd);
|
|
3548
3759
|
const repairedLines = block.split("\n").map(repairFrontmatterLine);
|
|
3549
3760
|
const repairedBlock = repairedLines.map(({ line }) => line).join("\n");
|
|
@@ -5222,42 +5433,6 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
|
|
|
5222
5433
|
*/
|
|
5223
5434
|
const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5224
5435
|
//#endregion
|
|
5225
|
-
//#region src/utils/prototype-pollution.ts
|
|
5226
|
-
/**
|
|
5227
|
-
* Keys that, if walked into when constructing or merging objects from
|
|
5228
|
-
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
5229
|
-
* chain) and propagate state to every other object in the runtime. Any code
|
|
5230
|
-
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
5231
|
-
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
5232
|
-
*/
|
|
5233
|
-
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
5234
|
-
"__proto__",
|
|
5235
|
-
"constructor",
|
|
5236
|
-
"prototype"
|
|
5237
|
-
]);
|
|
5238
|
-
function isPrototypePollutionKey(key) {
|
|
5239
|
-
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
5240
|
-
}
|
|
5241
|
-
/**
|
|
5242
|
-
* Returns a shallow copy of a record's own entries with every
|
|
5243
|
-
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
5244
|
-
*
|
|
5245
|
-
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
5246
|
-
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
5247
|
-
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
5248
|
-
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
5249
|
-
* prototype). Walking the entries through this helper severs that path while
|
|
5250
|
-
* preserving every legitimate key.
|
|
5251
|
-
*/
|
|
5252
|
-
function omitPrototypePollutionKeys(record) {
|
|
5253
|
-
const sanitized = {};
|
|
5254
|
-
for (const [key, value] of Object.entries(record)) {
|
|
5255
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
5256
|
-
sanitized[key] = value;
|
|
5257
|
-
}
|
|
5258
|
-
return sanitized;
|
|
5259
|
-
}
|
|
5260
|
-
//#endregion
|
|
5261
5436
|
//#region src/utils/jsonc.ts
|
|
5262
5437
|
/**
|
|
5263
5438
|
* Rebuild the parsed value from its own enumerable entries, dropping
|
|
@@ -6008,6 +6183,17 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6008
6183
|
const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
|
|
6009
6184
|
throw new RulesyncSourceNotFoundError(`No ${(0, node_path.join)(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath)} found.`);
|
|
6010
6185
|
}
|
|
6186
|
+
/**
|
|
6187
|
+
* Return one server exactly as authored, before `getMcpServers()` strips
|
|
6188
|
+
* rulesync- and tool-specific fields. Keep this lookup here so every target
|
|
6189
|
+
* that re-merges one of those fields shares the same own-property and
|
|
6190
|
+
* prototype-pollution guards.
|
|
6191
|
+
*/
|
|
6192
|
+
getRawMcpServer(name) {
|
|
6193
|
+
if (isPrototypePollutionKey(name)) return void 0;
|
|
6194
|
+
const mcpServers = isRecord$1(this.json) ? this.json.mcpServers : void 0;
|
|
6195
|
+
return isRecord$1(mcpServers) && Object.hasOwn(mcpServers, name) ? mcpServers[name] : void 0;
|
|
6196
|
+
}
|
|
6011
6197
|
getMcpServers() {
|
|
6012
6198
|
const mcpServers = this.json.mcpServers ?? {};
|
|
6013
6199
|
const entries = Object.entries(mcpServers);
|
|
@@ -7634,6 +7820,11 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
7634
7820
|
globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7635
7821
|
agentsmd: zod_mini.z.optional(zod_mini.z.looseObject({ subprojectPath: zod_mini.z.optional(zod_mini.z.string()) })),
|
|
7636
7822
|
claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
|
|
7823
|
+
codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7824
|
+
paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7825
|
+
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7826
|
+
description: zod_mini.z.optional(zod_mini.z.string())
|
|
7827
|
+
})),
|
|
7637
7828
|
cursor: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7638
7829
|
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7639
7830
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -7675,7 +7866,8 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
7675
7866
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
7676
7867
|
extends: zod_mini.z.optional(zod_mini.z.string()),
|
|
7677
7868
|
facet: zod_mini.z.optional(zod_mini.z.enum(["policies", "output-contracts"]))
|
|
7678
|
-
}))
|
|
7869
|
+
})),
|
|
7870
|
+
factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({ channel: zod_mini.z.optional(zod_mini.z.enum(["design"])) }))
|
|
7679
7871
|
});
|
|
7680
7872
|
/**
|
|
7681
7873
|
* The `agentsmd.subprojectPath` every consumer should act on, resolved once so
|
|
@@ -8790,15 +8982,15 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8790
8982
|
})),
|
|
8791
8983
|
opencode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8792
8984
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8793
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
8794
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
8795
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
8985
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8986
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8987
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
8796
8988
|
})),
|
|
8797
8989
|
kilo: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8798
8990
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8799
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
8800
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
8801
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
8991
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8992
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8993
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
8802
8994
|
})),
|
|
8803
8995
|
kiro: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8804
8996
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -8807,9 +8999,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8807
8999
|
})),
|
|
8808
9000
|
deepagents: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8809
9001
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8810
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
8811
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
8812
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
9002
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
9003
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
9004
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
8813
9005
|
})),
|
|
8814
9006
|
copilot: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8815
9007
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -8916,6 +9108,13 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8916
9108
|
takt: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8917
9109
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
8918
9110
|
extends: zod_mini.z.optional(zod_mini.z.string())
|
|
9111
|
+
})),
|
|
9112
|
+
crush: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
9113
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9114
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9115
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
9116
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
9117
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
8919
9118
|
}))
|
|
8920
9119
|
});
|
|
8921
9120
|
/**
|
|
@@ -9141,7 +9340,7 @@ async function getLocalSkillDirNames(sourceTree) {
|
|
|
9141
9340
|
*
|
|
9142
9341
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
9143
9342
|
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
9144
|
-
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9343
|
+
* copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9145
9344
|
* default with a per-target value. A defined section value (including `false`)
|
|
9146
9345
|
* always wins over the root default.
|
|
9147
9346
|
*
|
|
@@ -9160,7 +9359,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
9160
9359
|
*
|
|
9161
9360
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
9162
9361
|
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
9163
|
-
* copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9362
|
+
* copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9164
9363
|
* per-target value. A defined section value (including `false`) always wins
|
|
9165
9364
|
* over the root default.
|
|
9166
9365
|
*
|
|
@@ -9625,8 +9824,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
9625
9824
|
* This only deletes files that are no longer in the rulesync source, not files that will be overwritten.
|
|
9626
9825
|
*/
|
|
9627
9826
|
async removeOrphanAiFiles(existingFiles, generatedFiles) {
|
|
9628
|
-
const generatedPaths = new Set(generatedFiles.map((f) => f.getFilePath()));
|
|
9629
|
-
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(f.getFilePath()));
|
|
9827
|
+
const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
|
|
9828
|
+
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
|
|
9630
9829
|
for (const aiFile of orphanFiles) {
|
|
9631
9830
|
const filePath = aiFile.getFilePath();
|
|
9632
9831
|
const loggedPath = stripControlCharacters(filePath);
|
|
@@ -10753,6 +10952,15 @@ const FACTORYDROID_COMMANDS_DIR_PATH = (0, node_path.join)(FACTORYDROID_DIR, "co
|
|
|
10753
10952
|
const FACTORYDROID_SKILLS_DIR_PATH = (0, node_path.join)(FACTORYDROID_DIR, "skills");
|
|
10754
10953
|
const FACTORYDROID_DROIDS_DIR_PATH = (0, node_path.join)(FACTORYDROID_DIR, "droids");
|
|
10755
10954
|
const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
|
|
10955
|
+
/**
|
|
10956
|
+
* Factory Droid's design-guidelines instruction file: "Always-on design-system,
|
|
10957
|
+
* UX, visual, and interaction guidance", loaded separately from `AGENTS.md`'s
|
|
10958
|
+
* coding guidelines. Project scope only — Factory's docs describe root and
|
|
10959
|
+
* nested `DESIGN.md` files like `AGENTS.md`, but document no personal/global
|
|
10960
|
+
* home-directory equivalent.
|
|
10961
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
10962
|
+
*/
|
|
10963
|
+
const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
|
|
10756
10964
|
const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
|
|
10757
10965
|
const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
|
|
10758
10966
|
const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
@@ -15652,6 +15860,10 @@ function toAllowedToolsArray(value) {
|
|
|
15652
15860
|
* The spec types `compatibility` as a free-form string. An object from a legacy
|
|
15653
15861
|
* rulesync input is flattened to `key: value` pairs instead of being emitted as
|
|
15654
15862
|
* a YAML mapping, which conformant clients reject.
|
|
15863
|
+
*
|
|
15864
|
+
* Exported for `CrushSkill`, which requires the same bare-string shape (Crush's
|
|
15865
|
+
* Go struct types `Compatibility` as a plain `string`) and reuses this
|
|
15866
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15655
15867
|
*/
|
|
15656
15868
|
function toCompatibilityString(value) {
|
|
15657
15869
|
if (typeof value === "string") return value;
|
|
@@ -15660,6 +15872,10 @@ function toCompatibilityString(value) {
|
|
|
15660
15872
|
/**
|
|
15661
15873
|
* The spec types `metadata` as "a map from string keys to string values", so
|
|
15662
15874
|
* non-string values (e.g. a YAML number `version: 1`) are stringified.
|
|
15875
|
+
*
|
|
15876
|
+
* Exported for `CrushSkill`, which requires the same `map[string]string`
|
|
15877
|
+
* shape (Crush's Go struct types `Metadata` that way) and reuses this
|
|
15878
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15663
15879
|
*/
|
|
15664
15880
|
function toStringMetadata(metadata) {
|
|
15665
15881
|
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
|
|
@@ -18670,7 +18886,13 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18670
18886
|
if (!matchByBasename || flatOnly && (0, node_path.dirname)(key) !== ".") return [key];
|
|
18671
18887
|
return [key, (0, node_path.basename)(key)];
|
|
18672
18888
|
};
|
|
18673
|
-
const
|
|
18889
|
+
const claimedKeys = new ClaimedIdentities();
|
|
18890
|
+
const primarySource = paths.relativeDirPath;
|
|
18891
|
+
const secondarySource = "a secondary source";
|
|
18892
|
+
for (const command of toolCommands) for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18893
|
+
identity: candidate,
|
|
18894
|
+
source: primarySource
|
|
18895
|
+
});
|
|
18674
18896
|
const additionalCommands = await factory.class.loadAdditionalImportFiles({
|
|
18675
18897
|
outputRoot: this.outputRoot,
|
|
18676
18898
|
global: this.global,
|
|
@@ -18678,11 +18900,26 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18678
18900
|
});
|
|
18679
18901
|
for (const command of additionalCommands) {
|
|
18680
18902
|
const key = command.getRelativeFilePath();
|
|
18681
|
-
|
|
18682
|
-
|
|
18903
|
+
const collision = [...new Set(keysOf(command, true))].map((candidate) => {
|
|
18904
|
+
const claimed = claimedKeys.claim({
|
|
18905
|
+
identity: candidate,
|
|
18906
|
+
source: secondarySource
|
|
18907
|
+
});
|
|
18908
|
+
return claimed === null ? void 0 : {
|
|
18909
|
+
candidate,
|
|
18910
|
+
claimed
|
|
18911
|
+
};
|
|
18912
|
+
}).find((hit) => hit !== void 0);
|
|
18913
|
+
if (collision) {
|
|
18914
|
+
const { candidate, claimed } = collision;
|
|
18915
|
+
if (claimed.spelling === candidate) this.logger.warn(`Duplicate ${this.toolTarget} command "${stripControlCharacters(key)}" from ${secondarySource}; keeping the one already loaded.`);
|
|
18916
|
+
else this.logger.warn(`Case-insensitive ${this.toolTarget} command collision: "${stripControlCharacters(claimed.spelling)}" and "${stripControlCharacters(candidate)}" resolve to the same command file. Keeping "${stripControlCharacters(claimed.spelling)}" from ${claimed.source === secondarySource ? "earlier in the same source" : `the higher-precedence ${claimed.source}`} and ignoring "${stripControlCharacters(key)}" from ${secondarySource}, which is not imported.`);
|
|
18683
18917
|
continue;
|
|
18684
18918
|
}
|
|
18685
|
-
for (const candidate of keysOf(command))
|
|
18919
|
+
for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18920
|
+
identity: candidate,
|
|
18921
|
+
source: secondarySource
|
|
18922
|
+
});
|
|
18686
18923
|
toolCommands.push(command);
|
|
18687
18924
|
}
|
|
18688
18925
|
}
|
|
@@ -18972,6 +19209,21 @@ var AmpHooks = class AmpHooks extends ToolHooks {
|
|
|
18972
19209
|
}
|
|
18973
19210
|
};
|
|
18974
19211
|
//#endregion
|
|
19212
|
+
//#region src/utils/own-lookup.ts
|
|
19213
|
+
/**
|
|
19214
|
+
* Read a key from a plain string map without walking its prototype chain.
|
|
19215
|
+
*
|
|
19216
|
+
* A bracket read on an object literal resolves inherited members too, so a
|
|
19217
|
+
* user-supplied key such as `toString` or `constructor` "succeeds" with an
|
|
19218
|
+
* `Object.prototype` function instead of falling through to the caller's
|
|
19219
|
+
* `?? fallback`. Hook adapters translate native event names this way from
|
|
19220
|
+
* `Object.entries()` over a config file, so route the read through here to keep
|
|
19221
|
+
* the fallback honest: only a key the map itself defines yields a value.
|
|
19222
|
+
*/
|
|
19223
|
+
function lookupOwn({ record, key }) {
|
|
19224
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
19225
|
+
}
|
|
19226
|
+
//#endregion
|
|
18975
19227
|
//#region src/utils/object.ts
|
|
18976
19228
|
/**
|
|
18977
19229
|
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
@@ -19356,7 +19608,10 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
19356
19608
|
const warn = warnOnce(logger);
|
|
19357
19609
|
const result = {};
|
|
19358
19610
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
19359
|
-
const toolEventName =
|
|
19611
|
+
const toolEventName = lookupOwn({
|
|
19612
|
+
record: converterConfig.canonicalToToolEventNames,
|
|
19613
|
+
key: eventName
|
|
19614
|
+
}) ?? eventName;
|
|
19360
19615
|
const byMatcher = groupDefinitionsByMatcher({
|
|
19361
19616
|
definitions,
|
|
19362
19617
|
converterConfig
|
|
@@ -19784,7 +20039,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
19784
20039
|
const warn = warnOnce(logger);
|
|
19785
20040
|
const canonical = {};
|
|
19786
20041
|
for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
|
|
19787
|
-
const eventName =
|
|
20042
|
+
const eventName = lookupOwn({
|
|
20043
|
+
record: converterConfig.toolToCanonicalEventNames,
|
|
20044
|
+
key: toolEventName
|
|
20045
|
+
}) ?? toolEventName;
|
|
19788
20046
|
if (!Array.isArray(matcherEntries)) continue;
|
|
19789
20047
|
const defs = [];
|
|
19790
20048
|
for (const rawEntry of matcherEntries) {
|
|
@@ -19840,7 +20098,10 @@ function flattenAntigravityHooks(parsed) {
|
|
|
19840
20098
|
const flat = {};
|
|
19841
20099
|
const addEvent = (event, entries) => {
|
|
19842
20100
|
if (isPrototypePollutionKey(event) || !Array.isArray(entries)) return;
|
|
19843
|
-
const existing =
|
|
20101
|
+
const existing = lookupOwn({
|
|
20102
|
+
record: flat,
|
|
20103
|
+
key: event
|
|
20104
|
+
});
|
|
19844
20105
|
flat[event] = existing ? [...existing, ...entries] : [...entries];
|
|
19845
20106
|
};
|
|
19846
20107
|
for (const [key, value] of Object.entries(parsed)) if (Array.isArray(value)) addEvent(key, value);
|
|
@@ -20227,6 +20488,16 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20227
20488
|
const paths = AugmentcodeHooks.getSettablePaths({ global });
|
|
20228
20489
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
20229
20490
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
20491
|
+
let existingHooks = {};
|
|
20492
|
+
try {
|
|
20493
|
+
const parsed = JSON.parse(existingContent);
|
|
20494
|
+
const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
|
|
20495
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
|
|
20496
|
+
} catch {
|
|
20497
|
+
existingHooks = {};
|
|
20498
|
+
}
|
|
20499
|
+
const nativeEventKeys = new Set(Object.values(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES));
|
|
20500
|
+
const preservedHooks = Object.fromEntries(Object.entries(existingHooks).filter(([key]) => !nativeEventKeys.has(key)));
|
|
20230
20501
|
const config = rulesyncHooks.getJson();
|
|
20231
20502
|
const augmentHooks = canonicalToToolHooks({
|
|
20232
20503
|
config,
|
|
@@ -20238,7 +20509,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20238
20509
|
fileKey: sharedConfigFileKey(paths),
|
|
20239
20510
|
feature: "hooks",
|
|
20240
20511
|
existingContent,
|
|
20241
|
-
patch: { hooks:
|
|
20512
|
+
patch: { hooks: {
|
|
20513
|
+
...preservedHooks,
|
|
20514
|
+
...augmentHooks
|
|
20515
|
+
} },
|
|
20242
20516
|
filePath
|
|
20243
20517
|
});
|
|
20244
20518
|
return new AugmentcodeHooks({
|
|
@@ -20996,7 +21270,10 @@ function canonicalToCopilotHooks(config) {
|
|
|
20996
21270
|
};
|
|
20997
21271
|
const copilot = {};
|
|
20998
21272
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
20999
|
-
const copilotEventName =
|
|
21273
|
+
const copilotEventName = lookupOwn({
|
|
21274
|
+
record: CANONICAL_TO_COPILOT_EVENT_NAMES,
|
|
21275
|
+
key: eventName
|
|
21276
|
+
}) ?? eventName;
|
|
21000
21277
|
const entries = [];
|
|
21001
21278
|
for (const def of definitions) {
|
|
21002
21279
|
const hookType = def.type ?? "command";
|
|
@@ -21073,7 +21350,10 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
21073
21350
|
if (copilotHooks === null || copilotHooks === void 0 || typeof copilotHooks !== "object") return {};
|
|
21074
21351
|
const canonical = {};
|
|
21075
21352
|
for (const [copilotEventName, hookEntries] of Object.entries(copilotHooks)) {
|
|
21076
|
-
const eventName =
|
|
21353
|
+
const eventName = lookupOwn({
|
|
21354
|
+
record: COPILOT_TO_CANONICAL_EVENT_NAMES,
|
|
21355
|
+
key: copilotEventName
|
|
21356
|
+
}) ?? copilotEventName;
|
|
21077
21357
|
if (!Array.isArray(hookEntries)) continue;
|
|
21078
21358
|
const defs = [];
|
|
21079
21359
|
for (const rawEntry of hookEntries) {
|
|
@@ -21343,7 +21623,10 @@ function canonicalToCopilotCliHooks(config, logger) {
|
|
|
21343
21623
|
};
|
|
21344
21624
|
const out = {};
|
|
21345
21625
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
21346
|
-
const copilotEventName =
|
|
21626
|
+
const copilotEventName = lookupOwn({
|
|
21627
|
+
record: CANONICAL_TO_COPILOTCLI_EVENT_NAMES,
|
|
21628
|
+
key: eventName
|
|
21629
|
+
}) ?? eventName;
|
|
21347
21630
|
const entries = buildCopilotCliEntriesForEvent({
|
|
21348
21631
|
eventName,
|
|
21349
21632
|
definitions,
|
|
@@ -21400,7 +21683,10 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
|
|
|
21400
21683
|
if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
|
|
21401
21684
|
const canonical = {};
|
|
21402
21685
|
for (const [copilotEventName, hookEntries] of Object.entries(rawHooks)) {
|
|
21403
|
-
const eventName =
|
|
21686
|
+
const eventName = lookupOwn({
|
|
21687
|
+
record: COPILOTCLI_TO_CANONICAL_EVENT_NAMES,
|
|
21688
|
+
key: copilotEventName
|
|
21689
|
+
}) ?? copilotEventName;
|
|
21404
21690
|
if (!Array.isArray(hookEntries)) continue;
|
|
21405
21691
|
const defs = [];
|
|
21406
21692
|
for (const rawEntry of hookEntries) {
|
|
@@ -21552,7 +21838,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21552
21838
|
const mappedHooks = {};
|
|
21553
21839
|
const cursorSupportedTypes = /* @__PURE__ */ new Set(["command", "prompt"]);
|
|
21554
21840
|
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
21555
|
-
const cursorEventName =
|
|
21841
|
+
const cursorEventName = lookupOwn({
|
|
21842
|
+
record: CANONICAL_TO_CURSOR_EVENT_NAMES,
|
|
21843
|
+
key: eventName
|
|
21844
|
+
}) ?? eventName;
|
|
21556
21845
|
const mappedDefs = defs.filter((def) => cursorSupportedTypes.has(def.type ?? "command")).map((def) => ({
|
|
21557
21846
|
...def.type !== void 0 && def.type !== null && { type: def.type },
|
|
21558
21847
|
...def.command !== void 0 && def.command !== null && { command: def.command },
|
|
@@ -21585,7 +21874,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21585
21874
|
const cursorHooks = parsed.hooks ?? {};
|
|
21586
21875
|
const canonicalHooks = {};
|
|
21587
21876
|
for (const [cursorEventName, defs] of Object.entries(cursorHooks)) {
|
|
21588
|
-
const eventName =
|
|
21877
|
+
const eventName = lookupOwn({
|
|
21878
|
+
record: CURSOR_TO_CANONICAL_EVENT_NAMES,
|
|
21879
|
+
key: cursorEventName
|
|
21880
|
+
}) ?? cursorEventName;
|
|
21589
21881
|
canonicalHooks[eventName] = defs;
|
|
21590
21882
|
}
|
|
21591
21883
|
const version = parsed.version ?? 1;
|
|
@@ -21655,7 +21947,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21655
21947
|
const hooks = {};
|
|
21656
21948
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
21657
21949
|
if (!supported.has(canonicalEvent)) continue;
|
|
21658
|
-
const deepagentsEvent =
|
|
21950
|
+
const deepagentsEvent = lookupOwn({
|
|
21951
|
+
record: CANONICAL_TO_DEEPAGENTS_EVENT_NAMES,
|
|
21952
|
+
key: canonicalEvent
|
|
21953
|
+
});
|
|
21659
21954
|
if (!deepagentsEvent) continue;
|
|
21660
21955
|
for (const def of definitions) {
|
|
21661
21956
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -21684,7 +21979,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21684
21979
|
function deepagentsToCanonicalHooks(hooks) {
|
|
21685
21980
|
const canonical = {};
|
|
21686
21981
|
for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
|
|
21687
|
-
const canonicalEvent =
|
|
21982
|
+
const canonicalEvent = lookupOwn({
|
|
21983
|
+
record: DEEPAGENTS_TO_CANONICAL_EVENT_NAMES,
|
|
21984
|
+
key: deepagentsEvent
|
|
21985
|
+
});
|
|
21688
21986
|
if (!canonicalEvent || !Array.isArray(groups)) continue;
|
|
21689
21987
|
for (const group of groups) {
|
|
21690
21988
|
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
@@ -21717,7 +22015,10 @@ function deepagentsLegacyToCanonicalHooks(entries) {
|
|
|
21717
22015
|
const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
|
|
21718
22016
|
const events = Array.isArray(entry.events) ? entry.events : [];
|
|
21719
22017
|
for (const legacyEvent of events) {
|
|
21720
|
-
const canonicalEvent = typeof legacyEvent === "string" ?
|
|
22018
|
+
const canonicalEvent = typeof legacyEvent === "string" ? lookupOwn({
|
|
22019
|
+
record: DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES,
|
|
22020
|
+
key: legacyEvent
|
|
22021
|
+
}) : void 0;
|
|
21721
22022
|
if (!canonicalEvent) continue;
|
|
21722
22023
|
(canonical[canonicalEvent] ??= []).push({
|
|
21723
22024
|
type: "command",
|
|
@@ -22438,7 +22739,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22438
22739
|
const result = {};
|
|
22439
22740
|
for (const [canonicalEvent, definitions] of Object.entries(config.hooks)) {
|
|
22440
22741
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22441
|
-
const nativeEvent =
|
|
22742
|
+
const nativeEvent = lookupOwn({
|
|
22743
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22744
|
+
key: canonicalEvent
|
|
22745
|
+
});
|
|
22442
22746
|
if (nativeEvent) setHermesHookEntries({
|
|
22443
22747
|
result,
|
|
22444
22748
|
event: nativeEvent,
|
|
@@ -22449,7 +22753,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22449
22753
|
}
|
|
22450
22754
|
for (const [canonicalEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
22451
22755
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22452
|
-
const nativeEvent =
|
|
22756
|
+
const nativeEvent = lookupOwn({
|
|
22757
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22758
|
+
key: canonicalEvent
|
|
22759
|
+
});
|
|
22453
22760
|
if (nativeEvent) setHermesHookEntries({
|
|
22454
22761
|
result,
|
|
22455
22762
|
event: nativeEvent,
|
|
@@ -22515,7 +22822,10 @@ function hermesHooksToCanonical(hooks) {
|
|
|
22515
22822
|
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
22516
22823
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
22517
22824
|
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
22518
|
-
const rulesyncEvent =
|
|
22825
|
+
const rulesyncEvent = lookupOwn({
|
|
22826
|
+
record: HERMESAGENT_TO_CANONICAL_EVENT_NAMES,
|
|
22827
|
+
key: nativeEvent
|
|
22828
|
+
}) ?? nativeEvent;
|
|
22519
22829
|
const defs = entries.map((raw) => hermesEntryToDefinition({
|
|
22520
22830
|
nativeEvent,
|
|
22521
22831
|
raw
|
|
@@ -23152,7 +23462,10 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
|
|
|
23152
23462
|
const result = [];
|
|
23153
23463
|
const nativeEvents = new Set(KIMI_CODE_NATIVE_HOOK_EVENTS);
|
|
23154
23464
|
for (const [event, definitions] of Object.entries(buildEffectiveHooks(config, toolOverrideHooks))) {
|
|
23155
|
-
const nativeEvent =
|
|
23465
|
+
const nativeEvent = lookupOwn({
|
|
23466
|
+
record: CANONICAL_TO_KIMI_CODE_EVENT_NAMES,
|
|
23467
|
+
key: event
|
|
23468
|
+
}) ?? event;
|
|
23156
23469
|
if (!nativeEvents.has(nativeEvent)) {
|
|
23157
23470
|
logger?.warn(`Kimi Code hooks: skipping unsupported event "${event}".`);
|
|
23158
23471
|
continue;
|
|
@@ -23186,14 +23499,22 @@ function kimiCodeHooksToCanonical(hooks) {
|
|
|
23186
23499
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
23187
23500
|
const entry = raw;
|
|
23188
23501
|
if (typeof entry.event !== "string" || typeof entry.command !== "string") continue;
|
|
23189
|
-
const event =
|
|
23502
|
+
const event = lookupOwn({
|
|
23503
|
+
record: KIMI_CODE_TO_CANONICAL_EVENT_NAMES,
|
|
23504
|
+
key: entry.event
|
|
23505
|
+
}) ?? entry.event;
|
|
23190
23506
|
const definition = {
|
|
23191
23507
|
type: "command",
|
|
23192
23508
|
command: stripTrustedDirectoryWrapper(entry.command),
|
|
23193
23509
|
...typeof entry.matcher === "string" && { matcher: entry.matcher },
|
|
23194
23510
|
...typeof entry.timeout === "number" && { timeout: entry.timeout }
|
|
23195
23511
|
};
|
|
23196
|
-
|
|
23512
|
+
const list = lookupOwn({
|
|
23513
|
+
record: result,
|
|
23514
|
+
key: event
|
|
23515
|
+
}) ?? [];
|
|
23516
|
+
list.push(definition);
|
|
23517
|
+
result[event] = list;
|
|
23197
23518
|
}
|
|
23198
23519
|
return result;
|
|
23199
23520
|
}
|
|
@@ -23396,7 +23717,13 @@ function canonicalToKiroIdeHooks(config) {
|
|
|
23396
23717
|
};
|
|
23397
23718
|
const entries = [];
|
|
23398
23719
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23399
|
-
const trigger =
|
|
23720
|
+
const trigger = lookupOwn({
|
|
23721
|
+
record: CANONICAL_TO_KIRO_IDE_EVENT_NAMES,
|
|
23722
|
+
key: eventName
|
|
23723
|
+
}) ?? lookupOwn({
|
|
23724
|
+
record: KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES,
|
|
23725
|
+
key: eventName
|
|
23726
|
+
}) ?? eventName;
|
|
23400
23727
|
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
23401
23728
|
}
|
|
23402
23729
|
return entries;
|
|
@@ -23405,7 +23732,10 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23405
23732
|
const canonical = {};
|
|
23406
23733
|
for (const entry of entries) {
|
|
23407
23734
|
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
23408
|
-
const eventName =
|
|
23735
|
+
const eventName = lookupOwn({
|
|
23736
|
+
record: KIRO_IDE_TO_CANONICAL_EVENT_NAMES,
|
|
23737
|
+
key: entry.trigger
|
|
23738
|
+
}) ?? entry.trigger;
|
|
23409
23739
|
if (isPrototypePollutionKey(eventName)) continue;
|
|
23410
23740
|
const def = {};
|
|
23411
23741
|
if (entry.action.type === "command") {
|
|
@@ -23422,7 +23752,12 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23422
23752
|
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
23423
23753
|
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
23424
23754
|
if (entry.enabled === false) def.enabled = false;
|
|
23425
|
-
|
|
23755
|
+
const list = lookupOwn({
|
|
23756
|
+
record: canonical,
|
|
23757
|
+
key: eventName
|
|
23758
|
+
}) ?? [];
|
|
23759
|
+
list.push(def);
|
|
23760
|
+
canonical[eventName] = list;
|
|
23426
23761
|
}
|
|
23427
23762
|
return canonical;
|
|
23428
23763
|
}
|
|
@@ -23603,7 +23938,10 @@ function canonicalToKiroHooks({ config, logger }) {
|
|
|
23603
23938
|
};
|
|
23604
23939
|
const kiro = {};
|
|
23605
23940
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23606
|
-
const kiroEventName =
|
|
23941
|
+
const kiroEventName = lookupOwn({
|
|
23942
|
+
record: CANONICAL_TO_KIRO_EVENT_NAMES,
|
|
23943
|
+
key: eventName
|
|
23944
|
+
}) ?? eventName;
|
|
23607
23945
|
const entries = buildKiroEntriesForEvent(definitions);
|
|
23608
23946
|
if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
|
|
23609
23947
|
else kiro[kiroEventName] = entries;
|
|
@@ -23634,7 +23972,10 @@ function kiroHooksToCanonical(kiroHooks) {
|
|
|
23634
23972
|
if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
|
|
23635
23973
|
const canonical = {};
|
|
23636
23974
|
for (const [kiroEventName, entries] of Object.entries(kiroHooks)) {
|
|
23637
|
-
const eventName =
|
|
23975
|
+
const eventName = lookupOwn({
|
|
23976
|
+
record: KIRO_TO_CANONICAL_EVENT_NAMES,
|
|
23977
|
+
key: kiroEventName
|
|
23978
|
+
}) ?? kiroEventName;
|
|
23638
23979
|
if (!Array.isArray(entries)) continue;
|
|
23639
23980
|
const defs = [];
|
|
23640
23981
|
for (const rawEntry of entries) {
|
|
@@ -24193,7 +24534,10 @@ function canonicalToQwencodeHooks(config, logger) {
|
|
|
24193
24534
|
]);
|
|
24194
24535
|
const qwencode = {};
|
|
24195
24536
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
24196
|
-
const qwencodeEventName =
|
|
24537
|
+
const qwencodeEventName = lookupOwn({
|
|
24538
|
+
record: CANONICAL_TO_QWENCODE_EVENT_NAMES,
|
|
24539
|
+
key: eventName
|
|
24540
|
+
}) ?? eventName;
|
|
24197
24541
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
24198
24542
|
for (const def of definitions) {
|
|
24199
24543
|
if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
|
|
@@ -24298,7 +24642,10 @@ function qwencodeHooksToCanonical(qwencodeHooks) {
|
|
|
24298
24642
|
if (qwencodeHooks === null || qwencodeHooks === void 0 || typeof qwencodeHooks !== "object") return {};
|
|
24299
24643
|
const canonical = {};
|
|
24300
24644
|
for (const [qwencodeEventName, matcherEntries] of Object.entries(qwencodeHooks)) {
|
|
24301
|
-
const eventName =
|
|
24645
|
+
const eventName = lookupOwn({
|
|
24646
|
+
record: QWENCODE_TO_CANONICAL_EVENT_NAMES,
|
|
24647
|
+
key: qwencodeEventName
|
|
24648
|
+
}) ?? qwencodeEventName;
|
|
24302
24649
|
if (!Array.isArray(matcherEntries)) continue;
|
|
24303
24650
|
const defs = [];
|
|
24304
24651
|
for (const rawEntry of matcherEntries) {
|
|
@@ -24413,7 +24760,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24413
24760
|
const result = {};
|
|
24414
24761
|
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
24415
24762
|
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
24416
|
-
const reasonixEvent =
|
|
24763
|
+
const reasonixEvent = lookupOwn({
|
|
24764
|
+
record: CANONICAL_TO_REASONIX_EVENT_NAMES,
|
|
24765
|
+
key: event
|
|
24766
|
+
}) ?? event;
|
|
24417
24767
|
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
24418
24768
|
const entries = [];
|
|
24419
24769
|
for (const def of defs) {
|
|
@@ -24426,7 +24776,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24426
24776
|
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
24427
24777
|
entries.push(entry);
|
|
24428
24778
|
}
|
|
24429
|
-
if (entries.length > 0) result[reasonixEvent] = [...
|
|
24779
|
+
if (entries.length > 0) result[reasonixEvent] = [...lookupOwn({
|
|
24780
|
+
record: result,
|
|
24781
|
+
key: reasonixEvent
|
|
24782
|
+
}) ?? [], ...entries];
|
|
24430
24783
|
}
|
|
24431
24784
|
return result;
|
|
24432
24785
|
}
|
|
@@ -24439,7 +24792,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24439
24792
|
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
24440
24793
|
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
24441
24794
|
if (!Array.isArray(rawEntries)) continue;
|
|
24442
|
-
const canonicalEvent =
|
|
24795
|
+
const canonicalEvent = lookupOwn({
|
|
24796
|
+
record: REASONIX_TO_CANONICAL_EVENT_NAMES,
|
|
24797
|
+
key: reasonixEvent
|
|
24798
|
+
}) ?? reasonixEvent;
|
|
24443
24799
|
const defs = [];
|
|
24444
24800
|
for (const rawEntry of rawEntries) {
|
|
24445
24801
|
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
@@ -24454,7 +24810,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24454
24810
|
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
24455
24811
|
defs.push(def);
|
|
24456
24812
|
}
|
|
24457
|
-
if (defs.length > 0) canonical[canonicalEvent] = [...
|
|
24813
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...lookupOwn({
|
|
24814
|
+
record: canonical,
|
|
24815
|
+
key: canonicalEvent
|
|
24816
|
+
}) ?? [], ...defs];
|
|
24458
24817
|
}
|
|
24459
24818
|
return canonical;
|
|
24460
24819
|
}
|
|
@@ -24582,7 +24941,10 @@ function canonicalToVibeHooks(config, toolOverride) {
|
|
|
24582
24941
|
const hooks = [];
|
|
24583
24942
|
for (const [event, defs] of Object.entries(effective)) {
|
|
24584
24943
|
if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
|
|
24585
|
-
const vibeEvent =
|
|
24944
|
+
const vibeEvent = lookupOwn({
|
|
24945
|
+
record: CANONICAL_TO_VIBE_EVENT_NAMES,
|
|
24946
|
+
key: event
|
|
24947
|
+
}) ?? event;
|
|
24586
24948
|
let index = 0;
|
|
24587
24949
|
for (const def of defs) {
|
|
24588
24950
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -24612,7 +24974,10 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
24612
24974
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
24613
24975
|
if (vibeEvent === void 0) return null;
|
|
24614
24976
|
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
24615
|
-
const canonicalEvent =
|
|
24977
|
+
const canonicalEvent = lookupOwn({
|
|
24978
|
+
record: VIBE_TO_CANONICAL_EVENT_NAMES,
|
|
24979
|
+
key: vibeEvent
|
|
24980
|
+
}) ?? vibeEvent;
|
|
24616
24981
|
const def = { type: "command" };
|
|
24617
24982
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
24618
24983
|
if (typeof entry.match === "string" && entry.match !== "" && entry.match !== "*") def.matcher = entry.match;
|
|
@@ -24637,7 +25002,10 @@ function vibeHooksToCanonical(parsed) {
|
|
|
24637
25002
|
for (const raw of rawHooks) {
|
|
24638
25003
|
const result = vibeEntryToCanonicalDef(raw);
|
|
24639
25004
|
if (result === null) continue;
|
|
24640
|
-
const list =
|
|
25005
|
+
const list = lookupOwn({
|
|
25006
|
+
record: canonical,
|
|
25007
|
+
key: result.canonicalEvent
|
|
25008
|
+
}) ?? [];
|
|
24641
25009
|
list.push(result.def);
|
|
24642
25010
|
canonical[result.canonicalEvent] = list;
|
|
24643
25011
|
}
|
|
@@ -25716,6 +26084,68 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
|
|
|
25716
26084
|
}
|
|
25717
26085
|
};
|
|
25718
26086
|
//#endregion
|
|
26087
|
+
//#region src/constants/crush-paths.ts
|
|
26088
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
26089
|
+
const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
|
|
26090
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
26091
|
+
const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
|
|
26092
|
+
const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
|
|
26093
|
+
//#endregion
|
|
26094
|
+
//#region src/features/ignore/crush-ignore.ts
|
|
26095
|
+
/**
|
|
26096
|
+
* Ignore generator for Crush.
|
|
26097
|
+
*
|
|
26098
|
+
* Crush excludes files from tool access via a `.crushignore` file, read
|
|
26099
|
+
* hierarchically (root and any subdirectory, the same way it walks
|
|
26100
|
+
* `.gitignore`) using gitignore syntax. Crush documents no global/user-scope
|
|
26101
|
+
* ignore file, so this is project-only.
|
|
26102
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/fsext/fileutil.go
|
|
26103
|
+
*/
|
|
26104
|
+
var CrushIgnore = class CrushIgnore extends ToolIgnore {
|
|
26105
|
+
static getSettablePaths() {
|
|
26106
|
+
return {
|
|
26107
|
+
relativeDirPath: ".",
|
|
26108
|
+
relativeFilePath: CRUSH_IGNORE_FILE_NAME
|
|
26109
|
+
};
|
|
26110
|
+
}
|
|
26111
|
+
toRulesyncIgnore() {
|
|
26112
|
+
return new RulesyncIgnore({
|
|
26113
|
+
outputRoot: ".",
|
|
26114
|
+
relativeDirPath: ".",
|
|
26115
|
+
relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
|
|
26116
|
+
fileContent: this.fileContent
|
|
26117
|
+
});
|
|
26118
|
+
}
|
|
26119
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
26120
|
+
const body = rulesyncIgnore.getFileContent();
|
|
26121
|
+
return new CrushIgnore({
|
|
26122
|
+
outputRoot,
|
|
26123
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26124
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26125
|
+
fileContent: body
|
|
26126
|
+
});
|
|
26127
|
+
}
|
|
26128
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
26129
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
26130
|
+
return new CrushIgnore({
|
|
26131
|
+
outputRoot,
|
|
26132
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26133
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26134
|
+
fileContent,
|
|
26135
|
+
validate
|
|
26136
|
+
});
|
|
26137
|
+
}
|
|
26138
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
26139
|
+
return new CrushIgnore({
|
|
26140
|
+
outputRoot,
|
|
26141
|
+
relativeDirPath,
|
|
26142
|
+
relativeFilePath,
|
|
26143
|
+
fileContent: "",
|
|
26144
|
+
validate: false
|
|
26145
|
+
});
|
|
26146
|
+
}
|
|
26147
|
+
};
|
|
26148
|
+
//#endregion
|
|
25719
26149
|
//#region src/features/ignore/cursor-ignore.ts
|
|
25720
26150
|
/**
|
|
25721
26151
|
* Cursor ignore adapter.
|
|
@@ -26712,6 +27142,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
26712
27142
|
["claudecode", { class: ClaudecodeIgnore }],
|
|
26713
27143
|
["claudecode-legacy", { class: ClaudecodeIgnore }],
|
|
26714
27144
|
["cline", { class: ClineIgnore }],
|
|
27145
|
+
["crush", { class: CrushIgnore }],
|
|
26715
27146
|
["cursor", { class: CursorIgnore }],
|
|
26716
27147
|
["hermesagent", { class: HermesagentIgnore }],
|
|
26717
27148
|
["junie", { class: JunieIgnore }],
|
|
@@ -28016,9 +28447,8 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
28016
28447
|
throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
|
|
28017
28448
|
}
|
|
28018
28449
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
28019
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
28020
28450
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
28021
|
-
const rawServer =
|
|
28451
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
28022
28452
|
return [serverName, {
|
|
28023
28453
|
...serverConfig,
|
|
28024
28454
|
...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
@@ -30981,9 +31411,8 @@ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
|
|
|
30981
31411
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
30982
31412
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
30983
31413
|
const existing = parseMusecodeSettings(existingContent, filePath);
|
|
30984
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
30985
31414
|
const converted = convertToMusecodeFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
30986
|
-
const rawServer =
|
|
31415
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
30987
31416
|
const mode = asMusecodeMode(isRecord$1(rawServer) ? rawServer.musecodeMode : void 0);
|
|
30988
31417
|
return [serverName, {
|
|
30989
31418
|
...serverConfig,
|
|
@@ -31932,6 +32361,66 @@ async function readRovodevConfigYaml({ outputRoot }) {
|
|
|
31932
32361
|
filePath: (0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
|
|
31933
32362
|
});
|
|
31934
32363
|
}
|
|
32364
|
+
/**
|
|
32365
|
+
* Decide the absolute path a configured `mcpConfigPath` would name in the
|
|
32366
|
+
* given scope, without yet checking whether that path stays inside it. Split
|
|
32367
|
+
* out of resolveRovodevMcpImportPath so each function's branching stays
|
|
32368
|
+
* within the project's complexity budget.
|
|
32369
|
+
*/
|
|
32370
|
+
function resolveMcpConfigCandidatePath({ outputRoot, global, normalizedPath, configuredPath }) {
|
|
32371
|
+
if (global && normalizedPath.startsWith("~/")) return { path: (0, node_path.resolve)(outputRoot, normalizedPath.slice(2)) };
|
|
32372
|
+
if (!global && normalizedPath.startsWith("~/")) return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in project scope. A home-anchored path cannot be imported as part of a project, so importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32373
|
+
if ((0, node_path.isAbsolute)(normalizedPath)) return { path: (0, node_path.resolve)(normalizedPath) };
|
|
32374
|
+
if (!global) return { path: (0, node_path.resolve)(outputRoot, normalizedPath) };
|
|
32375
|
+
return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in global scope. Only home-anchored or absolute paths can be imported safely, so importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32376
|
+
}
|
|
32377
|
+
/**
|
|
32378
|
+
* Resolve the active Rovo Dev MCP config without following a pointer outside
|
|
32379
|
+
* the import scope. The implementation is deliberately separate from
|
|
32380
|
+
* fromFile: it keeps path-policy decisions testable without changing the
|
|
32381
|
+
* public ToolMcp contract.
|
|
32382
|
+
*/
|
|
32383
|
+
async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger }) {
|
|
32384
|
+
const fallback = {
|
|
32385
|
+
filePath: (0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_MCP_FILE_NAME),
|
|
32386
|
+
relativeDirPath: ROVODEV_DIR,
|
|
32387
|
+
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32388
|
+
};
|
|
32389
|
+
const configuredPath = (config && isRecord$1(config.mcp) ? config.mcp : {}).mcpConfigPath;
|
|
32390
|
+
if (configuredPath === void 0) {
|
|
32391
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is unset in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)}, which may not be the file Rovo Dev reads.`);
|
|
32392
|
+
return fallback;
|
|
32393
|
+
}
|
|
32394
|
+
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
32395
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} must be a non-empty string. Importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32396
|
+
return fallback;
|
|
32397
|
+
}
|
|
32398
|
+
const normalizedPath = normalizeMcpConfigPathValue(configuredPath.trim());
|
|
32399
|
+
const candidateResult = resolveMcpConfigCandidatePath({
|
|
32400
|
+
outputRoot,
|
|
32401
|
+
global,
|
|
32402
|
+
normalizedPath,
|
|
32403
|
+
configuredPath
|
|
32404
|
+
});
|
|
32405
|
+
if ("rejectionMessage" in candidateResult) {
|
|
32406
|
+
logger?.warn(candidateResult.rejectionMessage);
|
|
32407
|
+
return fallback;
|
|
32408
|
+
}
|
|
32409
|
+
const candidatePath = candidateResult.path;
|
|
32410
|
+
const relativePath = (0, node_path.relative)((0, node_path.resolve)(outputRoot), candidatePath);
|
|
32411
|
+
if (relativePath === "" || splitPathSegments(normalizedPath).includes("..") || pathEscapesRoot(relativePath) || await resolvedPathEscapesRoot({
|
|
32412
|
+
rootPath: outputRoot,
|
|
32413
|
+
targetPath: candidatePath
|
|
32414
|
+
})) {
|
|
32415
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)}, which is outside the import scope or traverses a symbolic link. Importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32416
|
+
return fallback;
|
|
32417
|
+
}
|
|
32418
|
+
return {
|
|
32419
|
+
filePath: candidatePath,
|
|
32420
|
+
relativeDirPath: (0, node_path.dirname)(relativePath),
|
|
32421
|
+
relativeFilePath: (0, node_path.basename)(relativePath)
|
|
32422
|
+
};
|
|
32423
|
+
}
|
|
31935
32424
|
function disabledNamesOf(config) {
|
|
31936
32425
|
const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
|
|
31937
32426
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
@@ -32037,6 +32526,25 @@ function envVarMcpFileSpellings({ fileName }) {
|
|
|
32037
32526
|
return [`$HOME/${tail}`, `\${HOME}/${tail}`];
|
|
32038
32527
|
}
|
|
32039
32528
|
/**
|
|
32529
|
+
* Classify the existing `mcpConfigPath` without deciding how to report it.
|
|
32530
|
+
* Keep the known-file checks in this order: the generated file is valid in
|
|
32531
|
+
* either scope, while the documented default and environment-variable
|
|
32532
|
+
* spellings are global-only alternatives that need their own warnings.
|
|
32533
|
+
*/
|
|
32534
|
+
function classifyExistingPointer({ existing, global, outputRoot }) {
|
|
32535
|
+
if (existing === void 0) return { kind: "unset" };
|
|
32536
|
+
const normalized = typeof existing === "string" ? normalizeMcpConfigPathValue(existing) : void 0;
|
|
32537
|
+
const namesFile = (fileName) => normalized !== void 0 && mcpFileSpellings({
|
|
32538
|
+
fileName,
|
|
32539
|
+
global,
|
|
32540
|
+
outputRoot
|
|
32541
|
+
}).includes(normalized);
|
|
32542
|
+
if (namesFile("mcp.json")) return { kind: "already-generated" };
|
|
32543
|
+
if (global && namesFile(ROVODEV_ALTERNATE_MCP_FILE_NAME)) return { kind: "documented-default" };
|
|
32544
|
+
if (global && normalized !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalized)) return { kind: "env-var-spelling" };
|
|
32545
|
+
return { kind: "unrelated" };
|
|
32546
|
+
}
|
|
32547
|
+
/**
|
|
32040
32548
|
* Point `mcp.mcpConfigPath` at the `mcp.json` rulesync writes for this scope,
|
|
32041
32549
|
* and report whether the block gained a value it did not already carry.
|
|
32042
32550
|
*
|
|
@@ -32147,45 +32655,44 @@ function announcePointer({ global, logger }) {
|
|
|
32147
32655
|
async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outputRoot, logger }) {
|
|
32148
32656
|
const { pointer, configLabel, mcpLabel } = pointerLabels(global);
|
|
32149
32657
|
const existing = existingMcp.mcpConfigPath;
|
|
32150
|
-
const
|
|
32151
|
-
|
|
32152
|
-
fileName,
|
|
32658
|
+
const classification = classifyExistingPointer({
|
|
32659
|
+
existing,
|
|
32153
32660
|
global,
|
|
32154
32661
|
outputRoot
|
|
32155
|
-
})
|
|
32156
|
-
const pointsAtGeneratedFile = namesFile(ROVODEV_MCP_FILE_NAME);
|
|
32662
|
+
});
|
|
32157
32663
|
if (!hasLiveServers) {
|
|
32158
|
-
if (
|
|
32664
|
+
if (classification.kind === "already-generated") logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} points at ${mcpLabel}, which now has no enabled server. Rovo Dev reads MCP servers from that file and nowhere else, so ${global ? "Rovo Dev has" : "this project has"} no MCP servers at all until one targeting rovodev is added back — remove the mcp.mcpConfigPath line to fall back to ${global ? "Rovo Dev's own default" : "the global config"}.`);
|
|
32159
32665
|
return false;
|
|
32160
32666
|
}
|
|
32161
|
-
|
|
32162
|
-
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
|
|
32667
|
+
switch (classification.kind) {
|
|
32668
|
+
case "unset": {
|
|
32669
|
+
const displaced = global ? await describeDisplacedGlobalServers({ outputRoot }) : null;
|
|
32670
|
+
if (displaced !== null) {
|
|
32671
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath unset in ${configLabel}, because ${displaced}. Atlassian documents that path and ${mcpLabel} as two different defaults for the setting, and mcpConfigPath names one config rather than merging, so pointing it at ${mcpLabel} would stop those servers being read on every project. Move the ones you want to keep into .rulesync/mcp.jsonc — or none at all, if it turns out to hold nothing you need — then set mcp.mcpConfigPath to "${pointer}" yourself, which rulesync will not overwrite.`);
|
|
32672
|
+
return false;
|
|
32673
|
+
}
|
|
32674
|
+
existingMcp.mcpConfigPath = pointer;
|
|
32675
|
+
announcePointer({
|
|
32676
|
+
global,
|
|
32677
|
+
logger
|
|
32678
|
+
});
|
|
32679
|
+
return true;
|
|
32166
32680
|
}
|
|
32167
|
-
|
|
32168
|
-
|
|
32169
|
-
|
|
32170
|
-
|
|
32171
|
-
|
|
32172
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
|
|
32176
|
-
|
|
32177
|
-
|
|
32178
|
-
|
|
32179
|
-
logger
|
|
32180
|
-
|
|
32181
|
-
return false;
|
|
32182
|
-
}
|
|
32183
|
-
if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
|
|
32184
|
-
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${quoteValueForWarning(existing)}. That names ${mcpLabel} only if Rovo Dev expands environment variables in this setting, which Atlassian does not document — if it does not, the path resolves literally and Rovo Dev reads no MCP servers at all. Write "${pointer}" instead, the form its own documented default uses.`);
|
|
32185
|
-
return false;
|
|
32681
|
+
case "already-generated": return false;
|
|
32682
|
+
case "documented-default":
|
|
32683
|
+
await warnAtDocumentedDefault({
|
|
32684
|
+
existing,
|
|
32685
|
+
outputRoot,
|
|
32686
|
+
logger
|
|
32687
|
+
});
|
|
32688
|
+
return false;
|
|
32689
|
+
case "env-var-spelling":
|
|
32690
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${quoteValueForWarning(existing)}. That names ${mcpLabel} only if Rovo Dev expands environment variables in this setting, which Atlassian does not document — if it does not, the path resolves literally and Rovo Dev reads no MCP servers at all. Write "${pointer}" instead, the form its own documented default uses.`);
|
|
32691
|
+
return false;
|
|
32692
|
+
case "unrelated":
|
|
32693
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. Rovo Dev reads MCP servers from that path, so the generated ${mcpLabel} is unused until it is set to "${pointer}".`);
|
|
32694
|
+
return false;
|
|
32186
32695
|
}
|
|
32187
|
-
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. Rovo Dev reads MCP servers from that path, so the generated ${mcpLabel} is unused until it is set to "${pointer}".`);
|
|
32188
|
-
return false;
|
|
32189
32696
|
}
|
|
32190
32697
|
/**
|
|
32191
32698
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
@@ -32227,14 +32734,20 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32227
32734
|
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32228
32735
|
};
|
|
32229
32736
|
}
|
|
32230
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32231
|
-
const
|
|
32232
|
-
const
|
|
32737
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
32738
|
+
const rovodevConfig = await readRovodevConfigYaml({ outputRoot });
|
|
32739
|
+
const paths = await resolveRovodevMcpImportPath({
|
|
32740
|
+
outputRoot,
|
|
32741
|
+
global,
|
|
32742
|
+
config: rovodevConfig,
|
|
32743
|
+
logger
|
|
32744
|
+
});
|
|
32745
|
+
const json = parseRovodevMcpJson(await readFileContentOrNull(paths.filePath) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
|
|
32233
32746
|
const newJson = {
|
|
32234
32747
|
...json,
|
|
32235
32748
|
mcpServers: json.mcpServers ?? {}
|
|
32236
32749
|
};
|
|
32237
|
-
const disabledNames = disabledNamesOf(
|
|
32750
|
+
const disabledNames = disabledNamesOf(rovodevConfig);
|
|
32238
32751
|
if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
|
|
32239
32752
|
const servers = newJson.mcpServers;
|
|
32240
32753
|
for (const name of disabledNames) {
|
|
@@ -32264,9 +32777,8 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32264
32777
|
} catch {
|
|
32265
32778
|
canWriteDisableToggle = false;
|
|
32266
32779
|
}
|
|
32267
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
32268
32780
|
const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
|
|
32269
|
-
const rawServer =
|
|
32781
|
+
const rawServer = rulesyncMcp.getRawMcpServer(name);
|
|
32270
32782
|
const record = {
|
|
32271
32783
|
...server,
|
|
32272
32784
|
...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
|
|
@@ -34254,6 +34766,518 @@ function convertAmpToRulesync({ disable, permissions }) {
|
|
|
34254
34766
|
return { permission };
|
|
34255
34767
|
}
|
|
34256
34768
|
//#endregion
|
|
34769
|
+
//#region src/utils/glob.ts
|
|
34770
|
+
/**
|
|
34771
|
+
* Convert a glob-like pattern into an anchored regex source string.
|
|
34772
|
+
*
|
|
34773
|
+
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34774
|
+
* every other regex metacharacter is escaped so it matches literally. The
|
|
34775
|
+
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34776
|
+
* name?" rather than "does this appear somewhere in it?".
|
|
34777
|
+
*
|
|
34778
|
+
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34779
|
+
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34780
|
+
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34781
|
+
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34782
|
+
* has to say what the tool would read rather than what a glob means. Use
|
|
34783
|
+
* `matchesGlob` for an actual comparison.
|
|
34784
|
+
*/
|
|
34785
|
+
function globToAnchoredRegexSource(glob) {
|
|
34786
|
+
let source = "";
|
|
34787
|
+
for (const char of glob) if (char === "*") source += ".*";
|
|
34788
|
+
else if (char === "?") source += ".";
|
|
34789
|
+
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34790
|
+
else source += char;
|
|
34791
|
+
return `^${source}$`;
|
|
34792
|
+
}
|
|
34793
|
+
/**
|
|
34794
|
+
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34795
|
+
* bracket is never closed — in which case it is an ordinary character.
|
|
34796
|
+
*/
|
|
34797
|
+
function parseGlobClass(characters, start) {
|
|
34798
|
+
let index = start;
|
|
34799
|
+
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34800
|
+
if (negated) index += 1;
|
|
34801
|
+
const members = /* @__PURE__ */ new Set();
|
|
34802
|
+
const ranges = [];
|
|
34803
|
+
let first = true;
|
|
34804
|
+
while (index < characters.length) {
|
|
34805
|
+
const character = characters[index] ?? "";
|
|
34806
|
+
if (character === "]" && !first) return {
|
|
34807
|
+
step: {
|
|
34808
|
+
kind: "class",
|
|
34809
|
+
negated,
|
|
34810
|
+
members,
|
|
34811
|
+
ranges
|
|
34812
|
+
},
|
|
34813
|
+
next: index + 1
|
|
34814
|
+
};
|
|
34815
|
+
first = false;
|
|
34816
|
+
const high = characters[index + 2];
|
|
34817
|
+
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34818
|
+
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34819
|
+
index += 3;
|
|
34820
|
+
continue;
|
|
34821
|
+
}
|
|
34822
|
+
members.add(character);
|
|
34823
|
+
index += 1;
|
|
34824
|
+
}
|
|
34825
|
+
}
|
|
34826
|
+
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34827
|
+
function parseGlob(glob) {
|
|
34828
|
+
const characters = [...glob];
|
|
34829
|
+
const steps = [];
|
|
34830
|
+
let index = 0;
|
|
34831
|
+
let bracketsAreClosed = true;
|
|
34832
|
+
while (index < characters.length) {
|
|
34833
|
+
const character = characters[index] ?? "";
|
|
34834
|
+
index += 1;
|
|
34835
|
+
if (character === "*") {
|
|
34836
|
+
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34837
|
+
continue;
|
|
34838
|
+
}
|
|
34839
|
+
if (character === "?") {
|
|
34840
|
+
steps.push({ kind: "any" });
|
|
34841
|
+
continue;
|
|
34842
|
+
}
|
|
34843
|
+
if (character === "[" && bracketsAreClosed) {
|
|
34844
|
+
const parsed = parseGlobClass(characters, index);
|
|
34845
|
+
if (parsed === void 0) bracketsAreClosed = false;
|
|
34846
|
+
else {
|
|
34847
|
+
steps.push(parsed.step);
|
|
34848
|
+
index = parsed.next;
|
|
34849
|
+
continue;
|
|
34850
|
+
}
|
|
34851
|
+
}
|
|
34852
|
+
steps.push({
|
|
34853
|
+
kind: "literal",
|
|
34854
|
+
character
|
|
34855
|
+
});
|
|
34856
|
+
}
|
|
34857
|
+
return steps;
|
|
34858
|
+
}
|
|
34859
|
+
function matchesGlobStep(step, character) {
|
|
34860
|
+
if (step.kind === "star") return false;
|
|
34861
|
+
if (step.kind === "any") return true;
|
|
34862
|
+
if (step.kind === "literal") return step.character === character;
|
|
34863
|
+
const code = character.codePointAt(0) ?? 0;
|
|
34864
|
+
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34865
|
+
return step.negated ? !admitted : admitted;
|
|
34866
|
+
}
|
|
34867
|
+
/** Whether two single-character steps can both match one same character. */
|
|
34868
|
+
function stepsShareACharacter(left, right) {
|
|
34869
|
+
if (left.kind === "any" || right.kind === "any") return true;
|
|
34870
|
+
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34871
|
+
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34872
|
+
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34873
|
+
return true;
|
|
34874
|
+
}
|
|
34875
|
+
/** Whether every step from `index` on can match the empty string. */
|
|
34876
|
+
function isAllStars(steps, index) {
|
|
34877
|
+
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34878
|
+
return true;
|
|
34879
|
+
}
|
|
34880
|
+
/**
|
|
34881
|
+
* The most work one intersection walk will do, counted in cells times the cost
|
|
34882
|
+
* of one. Past it the two patterns are reported as intersecting without being
|
|
34883
|
+
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34884
|
+
* enough to reach this is pathological rather than a command anybody typed.
|
|
34885
|
+
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34886
|
+
* closed.
|
|
34887
|
+
*/
|
|
34888
|
+
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34889
|
+
/**
|
|
34890
|
+
* The most work a whole run of comparisons will do. A caller holding R
|
|
34891
|
+
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34892
|
+
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34893
|
+
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34894
|
+
* that together take minutes. The shared budget is spent down across the run
|
|
34895
|
+
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34896
|
+
* again the direction that withholds an `allow` rather than writing one.
|
|
34897
|
+
*/
|
|
34898
|
+
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34899
|
+
/**
|
|
34900
|
+
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34901
|
+
* filling the two rows the table is held in, and collecting the answer.
|
|
34902
|
+
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34903
|
+
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34904
|
+
* short allow rules is n squared comparisons that never spend the budget down
|
|
34905
|
+
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34906
|
+
* walk length on the same exhaustible resource.
|
|
34907
|
+
*
|
|
34908
|
+
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34909
|
+
* which lowers how many pairs a run compares from around a million to about
|
|
34910
|
+
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34911
|
+
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34912
|
+
* answer exhaustion gives everywhere else.
|
|
34913
|
+
*/
|
|
34914
|
+
const INTERSECTION_PAIR_COST = 64;
|
|
34915
|
+
/**
|
|
34916
|
+
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34917
|
+
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34918
|
+
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34919
|
+
* it.
|
|
34920
|
+
*/
|
|
34921
|
+
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34922
|
+
return { remaining };
|
|
34923
|
+
}
|
|
34924
|
+
/**
|
|
34925
|
+
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34926
|
+
* the same pattern against a whole list parses it once and reuses the result.
|
|
34927
|
+
*/
|
|
34928
|
+
function parseGlobPattern(glob) {
|
|
34929
|
+
const steps = parseGlob(glob);
|
|
34930
|
+
return {
|
|
34931
|
+
steps,
|
|
34932
|
+
maxRanges: maxRangeCount(steps)
|
|
34933
|
+
};
|
|
34934
|
+
}
|
|
34935
|
+
/**
|
|
34936
|
+
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34937
|
+
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34938
|
+
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34939
|
+
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34940
|
+
* than on cells.
|
|
34941
|
+
*/
|
|
34942
|
+
function maxRangeCount(steps) {
|
|
34943
|
+
let most = 0;
|
|
34944
|
+
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34945
|
+
return most;
|
|
34946
|
+
}
|
|
34947
|
+
/**
|
|
34948
|
+
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34949
|
+
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34950
|
+
* Once that budget is exhausted every further pair answers `true` without being
|
|
34951
|
+
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34952
|
+
* safe side.
|
|
34953
|
+
*/
|
|
34954
|
+
function parsedGlobsIntersect(left, right, budget) {
|
|
34955
|
+
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
34956
|
+
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
34957
|
+
const cost = rows.length * columns.length * cellCost;
|
|
34958
|
+
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
34959
|
+
if (budget !== void 0) {
|
|
34960
|
+
const charge = cost + INTERSECTION_PAIR_COST;
|
|
34961
|
+
if (charge > budget.remaining) {
|
|
34962
|
+
budget.remaining = 0;
|
|
34963
|
+
return true;
|
|
34964
|
+
}
|
|
34965
|
+
budget.remaining -= charge;
|
|
34966
|
+
}
|
|
34967
|
+
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
34968
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
34969
|
+
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
34970
|
+
row[columns.length] = isAllStars(rows, i);
|
|
34971
|
+
for (let j = columns.length - 1; j >= 0; j--) {
|
|
34972
|
+
const rowStep = rows[i];
|
|
34973
|
+
const columnStep = columns[j];
|
|
34974
|
+
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
34975
|
+
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
34976
|
+
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
34977
|
+
continue;
|
|
34978
|
+
}
|
|
34979
|
+
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
34980
|
+
}
|
|
34981
|
+
next = row;
|
|
34982
|
+
}
|
|
34983
|
+
return next[0] ?? false;
|
|
34984
|
+
}
|
|
34985
|
+
//#endregion
|
|
34986
|
+
//#region src/features/permissions/shell-command-categories.ts
|
|
34987
|
+
/** The canonical category that names a shell command's permissions. */
|
|
34988
|
+
const SHELL_PERMISSION_CATEGORY = "bash";
|
|
34989
|
+
/**
|
|
34990
|
+
* Collect the canonical rules that govern shell commands, for the adapters
|
|
34991
|
+
* whose tool models commands and nothing else.
|
|
34992
|
+
*
|
|
34993
|
+
* The `bash` category contributes every rule. The all-tools `*` category
|
|
34994
|
+
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
34995
|
+
* written there covers shell commands too, and dropping it inverts the
|
|
34996
|
+
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
34997
|
+
* an adapter that reads only `bash` auto-approves the very command the file
|
|
34998
|
+
* denies.
|
|
34999
|
+
*
|
|
35000
|
+
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35001
|
+
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35002
|
+
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35003
|
+
* it in the permissive direction would grant something the author never said
|
|
35004
|
+
* about commands. Both directions therefore fail closed.
|
|
35005
|
+
*/
|
|
35006
|
+
function collectShellCommandRules(permission) {
|
|
35007
|
+
const rules = [];
|
|
35008
|
+
const foreignRestrictingCategories = [];
|
|
35009
|
+
const ignoredAllToolsAllowPatterns = [];
|
|
35010
|
+
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35011
|
+
if (category === "bash") {
|
|
35012
|
+
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35013
|
+
pattern,
|
|
35014
|
+
action,
|
|
35015
|
+
fromAllToolsCategory: false
|
|
35016
|
+
});
|
|
35017
|
+
continue;
|
|
35018
|
+
}
|
|
35019
|
+
if (category === "*") {
|
|
35020
|
+
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35021
|
+
if (action === "allow") {
|
|
35022
|
+
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35023
|
+
continue;
|
|
35024
|
+
}
|
|
35025
|
+
rules.push({
|
|
35026
|
+
pattern,
|
|
35027
|
+
action,
|
|
35028
|
+
fromAllToolsCategory: true
|
|
35029
|
+
});
|
|
35030
|
+
}
|
|
35031
|
+
continue;
|
|
35032
|
+
}
|
|
35033
|
+
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35034
|
+
}
|
|
35035
|
+
return {
|
|
35036
|
+
rules,
|
|
35037
|
+
foreignRestrictingCategories,
|
|
35038
|
+
ignoredAllToolsAllowPatterns
|
|
35039
|
+
};
|
|
35040
|
+
}
|
|
35041
|
+
/**
|
|
35042
|
+
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35043
|
+
* which restrictions it cannot write name some of the same commands? The
|
|
35044
|
+
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35045
|
+
* written — so a caller can report both the allow rules it withheld and the
|
|
35046
|
+
* restrictions that withheld nothing.
|
|
35047
|
+
*
|
|
35048
|
+
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35049
|
+
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35050
|
+
* asking whether any one command matches both. Width does not enter into it: an
|
|
35051
|
+
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35052
|
+
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35053
|
+
* on every `git ... --force` command even though neither pattern covers the
|
|
35054
|
+
* other's spelling. Comparing only identical spellings would let the most
|
|
35055
|
+
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35056
|
+
*
|
|
35057
|
+
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35058
|
+
* the walk for the commonest case.
|
|
35059
|
+
*
|
|
35060
|
+
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35061
|
+
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35062
|
+
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35063
|
+
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35064
|
+
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35065
|
+
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35066
|
+
* withholds an allow rather than writing one the config restricts — see
|
|
35067
|
+
* `warpCommandPatternToGlob`.
|
|
35068
|
+
*/
|
|
35069
|
+
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35070
|
+
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35071
|
+
pattern,
|
|
35072
|
+
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35073
|
+
}));
|
|
35074
|
+
return (allowPattern) => {
|
|
35075
|
+
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35076
|
+
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35077
|
+
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35078
|
+
};
|
|
35079
|
+
}
|
|
35080
|
+
/**
|
|
35081
|
+
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35082
|
+
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35083
|
+
*
|
|
35084
|
+
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35085
|
+
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35086
|
+
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35087
|
+
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35088
|
+
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35089
|
+
*
|
|
35090
|
+
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35091
|
+
* so overlapping no allow rule says nothing is wrong with it.
|
|
35092
|
+
*/
|
|
35093
|
+
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35094
|
+
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35095
|
+
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35096
|
+
return (0, es_toolkit.uniq)(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35097
|
+
}
|
|
35098
|
+
/**
|
|
35099
|
+
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35100
|
+
* commands with those two tiers and nothing else.
|
|
35101
|
+
*
|
|
35102
|
+
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35103
|
+
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35104
|
+
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35105
|
+
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35106
|
+
* about would answer the prompt the author wanted.
|
|
35107
|
+
*
|
|
35108
|
+
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35109
|
+
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35110
|
+
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35111
|
+
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35112
|
+
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35113
|
+
* cannot be written it withholds the allow rules it covers instead, which
|
|
35114
|
+
* restricts in the same direction without touching the denylist.
|
|
35115
|
+
*
|
|
35116
|
+
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35117
|
+
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35118
|
+
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35119
|
+
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35120
|
+
* even where it is written: a pattern under `*` need not name a command —
|
|
35121
|
+
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35122
|
+
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35123
|
+
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35124
|
+
* command pattern is reported; failing open would not be.
|
|
35125
|
+
*
|
|
35126
|
+
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35127
|
+
* patterns are not globs.
|
|
35128
|
+
*/
|
|
35129
|
+
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35130
|
+
const deny = [];
|
|
35131
|
+
const unwrittenDenyPatterns = [];
|
|
35132
|
+
const restrictions = [];
|
|
35133
|
+
const writtenAllToolsDenyPatterns = [];
|
|
35134
|
+
const allToolsAskPatterns = [];
|
|
35135
|
+
for (const rule of rules) {
|
|
35136
|
+
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35137
|
+
if (action === "allow") continue;
|
|
35138
|
+
if (action !== "deny") {
|
|
35139
|
+
restrictions.push(rule);
|
|
35140
|
+
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35141
|
+
continue;
|
|
35142
|
+
}
|
|
35143
|
+
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35144
|
+
deny.push(pattern);
|
|
35145
|
+
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35146
|
+
} else unwrittenDenyPatterns.push(pattern);
|
|
35147
|
+
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35148
|
+
}
|
|
35149
|
+
const budget = createIntersectionBudget();
|
|
35150
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35151
|
+
normalizePattern,
|
|
35152
|
+
budget
|
|
35153
|
+
});
|
|
35154
|
+
const allow = [];
|
|
35155
|
+
const shadowedAllowPatterns = [];
|
|
35156
|
+
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35157
|
+
for (const { pattern, action } of rules) {
|
|
35158
|
+
if (action !== "allow") continue;
|
|
35159
|
+
const shadowing = shadowingRestrictions(pattern);
|
|
35160
|
+
if (shadowing.length > 0) {
|
|
35161
|
+
shadowedAllowPatterns.push(pattern);
|
|
35162
|
+
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35163
|
+
continue;
|
|
35164
|
+
}
|
|
35165
|
+
allow.push(pattern);
|
|
35166
|
+
}
|
|
35167
|
+
return {
|
|
35168
|
+
allow,
|
|
35169
|
+
deny,
|
|
35170
|
+
shadowedAllowPatterns,
|
|
35171
|
+
unwrittenDenyPatterns,
|
|
35172
|
+
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35173
|
+
rules,
|
|
35174
|
+
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35175
|
+
withholdingPatterns
|
|
35176
|
+
}),
|
|
35177
|
+
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35178
|
+
rules,
|
|
35179
|
+
allToolsPatterns: allToolsAskPatterns,
|
|
35180
|
+
withholdingPatterns
|
|
35181
|
+
}),
|
|
35182
|
+
intersectionBudgetExhausted: budget.remaining === 0
|
|
35183
|
+
};
|
|
35184
|
+
}
|
|
35185
|
+
/**
|
|
35186
|
+
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35187
|
+
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35188
|
+
* dropped in one is worded the same way in all.
|
|
35189
|
+
*/
|
|
35190
|
+
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35191
|
+
if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
|
|
35192
|
+
for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
|
|
35193
|
+
if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
|
|
35194
|
+
if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
|
|
35195
|
+
if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
|
|
35196
|
+
if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
|
|
35197
|
+
if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
|
|
35198
|
+
}
|
|
35199
|
+
function resolveShellCommandState(permission, writesAllToolsDeny) {
|
|
35200
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permission);
|
|
35201
|
+
const partitioned = partitionCommandRules({
|
|
35202
|
+
rules,
|
|
35203
|
+
writesAllToolsDeny
|
|
35204
|
+
});
|
|
35205
|
+
return {
|
|
35206
|
+
allow: partitioned.allow,
|
|
35207
|
+
deny: partitioned.deny,
|
|
35208
|
+
bash: bashRulesHonoringAllTools(permission),
|
|
35209
|
+
foreignRestrictingCategories,
|
|
35210
|
+
ignoredAllToolsAllowPatterns,
|
|
35211
|
+
shadowedAllowPatterns: partitioned.shadowedAllowPatterns,
|
|
35212
|
+
unwrittenDenyPatterns: partitioned.unwrittenDenyPatterns,
|
|
35213
|
+
unenforcedAllToolsDenyPatterns: partitioned.unenforcedAllToolsDenyPatterns,
|
|
35214
|
+
unenforcedAllToolsAskPatterns: partitioned.unenforcedAllToolsAskPatterns,
|
|
35215
|
+
intersectionBudgetExhausted: partitioned.intersectionBudgetExhausted
|
|
35216
|
+
};
|
|
35217
|
+
}
|
|
35218
|
+
/**
|
|
35219
|
+
* Collect shell-command allow/deny lists the way the command-only adapters do,
|
|
35220
|
+
* and report every restriction the surface cannot carry.
|
|
35221
|
+
*/
|
|
35222
|
+
function resolveShellCommandLists({ permission, writesAllToolsDeny, toolLabel, surfaceLabel, logger }) {
|
|
35223
|
+
const resolved = resolveShellCommandState(permission, writesAllToolsDeny);
|
|
35224
|
+
warnAboutUnwrittenCommandRules({
|
|
35225
|
+
toolLabel,
|
|
35226
|
+
surfaceLabel,
|
|
35227
|
+
foreignRestrictingCategories: resolved.foreignRestrictingCategories,
|
|
35228
|
+
shadowedAllowPatterns: resolved.shadowedAllowPatterns,
|
|
35229
|
+
unwrittenDenyPatterns: resolved.unwrittenDenyPatterns,
|
|
35230
|
+
unenforcedAllToolsDenyPatterns: resolved.unenforcedAllToolsDenyPatterns,
|
|
35231
|
+
unenforcedAllToolsAskPatterns: resolved.unenforcedAllToolsAskPatterns,
|
|
35232
|
+
ignoredAllToolsAllowPatterns: resolved.ignoredAllToolsAllowPatterns,
|
|
35233
|
+
intersectionBudgetExhausted: resolved.intersectionBudgetExhausted,
|
|
35234
|
+
logger
|
|
35235
|
+
});
|
|
35236
|
+
return {
|
|
35237
|
+
allow: resolved.allow,
|
|
35238
|
+
deny: resolved.deny,
|
|
35239
|
+
bash: resolved.bash
|
|
35240
|
+
};
|
|
35241
|
+
}
|
|
35242
|
+
/**
|
|
35243
|
+
* The `bash` category after all-tools `*` restrictions have been applied. A
|
|
35244
|
+
* `deny`/`ask` written under `*` covers shell commands too, so a bash `allow`
|
|
35245
|
+
* it overlaps is withheld, a `*` deny is copied in, and a `*` ask is copied in
|
|
35246
|
+
* wherever `bash` says nothing about that exact pattern yet — otherwise it
|
|
35247
|
+
* would vanish from the resolved category entirely rather than falling back to
|
|
35248
|
+
* a tier that still prompts. An existing `bash` entry for the same pattern is
|
|
35249
|
+
* never downgraded by a `*` ask (a bash `allow` was already dropped above, and
|
|
35250
|
+
* a bash `deny`/`ask` there is at least as strict already).
|
|
35251
|
+
*/
|
|
35252
|
+
function bashRulesHonoringAllTools(permission) {
|
|
35253
|
+
const { rules } = collectShellCommandRules(permission);
|
|
35254
|
+
const allToolsRestrictions = rules.filter(({ fromAllToolsCategory }) => fromAllToolsCategory);
|
|
35255
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(allToolsRestrictions);
|
|
35256
|
+
const bash = { ...permission.bash };
|
|
35257
|
+
for (const [pattern, action] of Object.entries(bash)) if (action === "allow" && shadowingRestrictions(pattern).length > 0) delete bash[pattern];
|
|
35258
|
+
for (const { pattern, action } of allToolsRestrictions) {
|
|
35259
|
+
if (isPrototypePollutionKey(pattern)) continue;
|
|
35260
|
+
if (action === "deny") {
|
|
35261
|
+
if (bash[pattern] !== "ask") bash[pattern] = "deny";
|
|
35262
|
+
continue;
|
|
35263
|
+
}
|
|
35264
|
+
if (bash[pattern] === void 0) bash[pattern] = "ask";
|
|
35265
|
+
}
|
|
35266
|
+
return bash;
|
|
35267
|
+
}
|
|
35268
|
+
/**
|
|
35269
|
+
* Return a permission block whose `bash` category honors all-tools `*`
|
|
35270
|
+
* restrictions. Other categories are unchanged, so adapters that already model
|
|
35271
|
+
* `*` keep doing so.
|
|
35272
|
+
*/
|
|
35273
|
+
function honorAllToolsOnBash(permission) {
|
|
35274
|
+
if (permission.bash === void 0) return permission;
|
|
35275
|
+
return {
|
|
35276
|
+
...permission,
|
|
35277
|
+
bash: bashRulesHonoringAllTools(permission)
|
|
35278
|
+
};
|
|
35279
|
+
}
|
|
35280
|
+
//#endregion
|
|
34257
35281
|
//#region src/features/permissions/antigravity-cli-permissions.ts
|
|
34258
35282
|
/**
|
|
34259
35283
|
* Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
|
|
@@ -34501,7 +35525,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
|
|
|
34501
35525
|
const allow = [];
|
|
34502
35526
|
const ask = [];
|
|
34503
35527
|
const deny = [];
|
|
34504
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35528
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34505
35529
|
const cliToolName = toAntigravityCliToolName(category);
|
|
34506
35530
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
34507
35531
|
const entry = buildPermissionEntry$1(cliToolName, pattern);
|
|
@@ -34712,7 +35736,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
|
|
|
34712
35736
|
const allow = [];
|
|
34713
35737
|
const ask = [];
|
|
34714
35738
|
const deny = [];
|
|
34715
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35739
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34716
35740
|
const action = toIdeAction(category);
|
|
34717
35741
|
for (const [pattern, permissionAction] of Object.entries(rules)) {
|
|
34718
35742
|
const entry = buildPermissionEntry(action, pattern);
|
|
@@ -34752,223 +35776,6 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
34752
35776
|
return { permission };
|
|
34753
35777
|
}
|
|
34754
35778
|
//#endregion
|
|
34755
|
-
//#region src/utils/glob.ts
|
|
34756
|
-
/**
|
|
34757
|
-
* Convert a glob-like pattern into an anchored regex source string.
|
|
34758
|
-
*
|
|
34759
|
-
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34760
|
-
* every other regex metacharacter is escaped so it matches literally. The
|
|
34761
|
-
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34762
|
-
* name?" rather than "does this appear somewhere in it?".
|
|
34763
|
-
*
|
|
34764
|
-
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34765
|
-
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34766
|
-
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34767
|
-
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34768
|
-
* has to say what the tool would read rather than what a glob means. Use
|
|
34769
|
-
* `matchesGlob` for an actual comparison.
|
|
34770
|
-
*/
|
|
34771
|
-
function globToAnchoredRegexSource(glob) {
|
|
34772
|
-
let source = "";
|
|
34773
|
-
for (const char of glob) if (char === "*") source += ".*";
|
|
34774
|
-
else if (char === "?") source += ".";
|
|
34775
|
-
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34776
|
-
else source += char;
|
|
34777
|
-
return `^${source}$`;
|
|
34778
|
-
}
|
|
34779
|
-
/**
|
|
34780
|
-
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34781
|
-
* bracket is never closed — in which case it is an ordinary character.
|
|
34782
|
-
*/
|
|
34783
|
-
function parseGlobClass(characters, start) {
|
|
34784
|
-
let index = start;
|
|
34785
|
-
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34786
|
-
if (negated) index += 1;
|
|
34787
|
-
const members = /* @__PURE__ */ new Set();
|
|
34788
|
-
const ranges = [];
|
|
34789
|
-
let first = true;
|
|
34790
|
-
while (index < characters.length) {
|
|
34791
|
-
const character = characters[index] ?? "";
|
|
34792
|
-
if (character === "]" && !first) return {
|
|
34793
|
-
step: {
|
|
34794
|
-
kind: "class",
|
|
34795
|
-
negated,
|
|
34796
|
-
members,
|
|
34797
|
-
ranges
|
|
34798
|
-
},
|
|
34799
|
-
next: index + 1
|
|
34800
|
-
};
|
|
34801
|
-
first = false;
|
|
34802
|
-
const high = characters[index + 2];
|
|
34803
|
-
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34804
|
-
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34805
|
-
index += 3;
|
|
34806
|
-
continue;
|
|
34807
|
-
}
|
|
34808
|
-
members.add(character);
|
|
34809
|
-
index += 1;
|
|
34810
|
-
}
|
|
34811
|
-
}
|
|
34812
|
-
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34813
|
-
function parseGlob(glob) {
|
|
34814
|
-
const characters = [...glob];
|
|
34815
|
-
const steps = [];
|
|
34816
|
-
let index = 0;
|
|
34817
|
-
let bracketsAreClosed = true;
|
|
34818
|
-
while (index < characters.length) {
|
|
34819
|
-
const character = characters[index] ?? "";
|
|
34820
|
-
index += 1;
|
|
34821
|
-
if (character === "*") {
|
|
34822
|
-
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34823
|
-
continue;
|
|
34824
|
-
}
|
|
34825
|
-
if (character === "?") {
|
|
34826
|
-
steps.push({ kind: "any" });
|
|
34827
|
-
continue;
|
|
34828
|
-
}
|
|
34829
|
-
if (character === "[" && bracketsAreClosed) {
|
|
34830
|
-
const parsed = parseGlobClass(characters, index);
|
|
34831
|
-
if (parsed === void 0) bracketsAreClosed = false;
|
|
34832
|
-
else {
|
|
34833
|
-
steps.push(parsed.step);
|
|
34834
|
-
index = parsed.next;
|
|
34835
|
-
continue;
|
|
34836
|
-
}
|
|
34837
|
-
}
|
|
34838
|
-
steps.push({
|
|
34839
|
-
kind: "literal",
|
|
34840
|
-
character
|
|
34841
|
-
});
|
|
34842
|
-
}
|
|
34843
|
-
return steps;
|
|
34844
|
-
}
|
|
34845
|
-
function matchesGlobStep(step, character) {
|
|
34846
|
-
if (step.kind === "star") return false;
|
|
34847
|
-
if (step.kind === "any") return true;
|
|
34848
|
-
if (step.kind === "literal") return step.character === character;
|
|
34849
|
-
const code = character.codePointAt(0) ?? 0;
|
|
34850
|
-
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34851
|
-
return step.negated ? !admitted : admitted;
|
|
34852
|
-
}
|
|
34853
|
-
/** Whether two single-character steps can both match one same character. */
|
|
34854
|
-
function stepsShareACharacter(left, right) {
|
|
34855
|
-
if (left.kind === "any" || right.kind === "any") return true;
|
|
34856
|
-
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34857
|
-
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34858
|
-
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34859
|
-
return true;
|
|
34860
|
-
}
|
|
34861
|
-
/** Whether every step from `index` on can match the empty string. */
|
|
34862
|
-
function isAllStars(steps, index) {
|
|
34863
|
-
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34864
|
-
return true;
|
|
34865
|
-
}
|
|
34866
|
-
/**
|
|
34867
|
-
* The most work one intersection walk will do, counted in cells times the cost
|
|
34868
|
-
* of one. Past it the two patterns are reported as intersecting without being
|
|
34869
|
-
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34870
|
-
* enough to reach this is pathological rather than a command anybody typed.
|
|
34871
|
-
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34872
|
-
* closed.
|
|
34873
|
-
*/
|
|
34874
|
-
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34875
|
-
/**
|
|
34876
|
-
* The most work a whole run of comparisons will do. A caller holding R
|
|
34877
|
-
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34878
|
-
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34879
|
-
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34880
|
-
* that together take minutes. The shared budget is spent down across the run
|
|
34881
|
-
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34882
|
-
* again the direction that withholds an `allow` rather than writing one.
|
|
34883
|
-
*/
|
|
34884
|
-
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34885
|
-
/**
|
|
34886
|
-
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34887
|
-
* filling the two rows the table is held in, and collecting the answer.
|
|
34888
|
-
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34889
|
-
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34890
|
-
* short allow rules is n squared comparisons that never spend the budget down
|
|
34891
|
-
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34892
|
-
* walk length on the same exhaustible resource.
|
|
34893
|
-
*
|
|
34894
|
-
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34895
|
-
* which lowers how many pairs a run compares from around a million to about
|
|
34896
|
-
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34897
|
-
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34898
|
-
* answer exhaustion gives everywhere else.
|
|
34899
|
-
*/
|
|
34900
|
-
const INTERSECTION_PAIR_COST = 64;
|
|
34901
|
-
/**
|
|
34902
|
-
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34903
|
-
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34904
|
-
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34905
|
-
* it.
|
|
34906
|
-
*/
|
|
34907
|
-
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34908
|
-
return { remaining };
|
|
34909
|
-
}
|
|
34910
|
-
/**
|
|
34911
|
-
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34912
|
-
* the same pattern against a whole list parses it once and reuses the result.
|
|
34913
|
-
*/
|
|
34914
|
-
function parseGlobPattern(glob) {
|
|
34915
|
-
const steps = parseGlob(glob);
|
|
34916
|
-
return {
|
|
34917
|
-
steps,
|
|
34918
|
-
maxRanges: maxRangeCount(steps)
|
|
34919
|
-
};
|
|
34920
|
-
}
|
|
34921
|
-
/**
|
|
34922
|
-
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34923
|
-
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34924
|
-
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34925
|
-
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34926
|
-
* than on cells.
|
|
34927
|
-
*/
|
|
34928
|
-
function maxRangeCount(steps) {
|
|
34929
|
-
let most = 0;
|
|
34930
|
-
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34931
|
-
return most;
|
|
34932
|
-
}
|
|
34933
|
-
/**
|
|
34934
|
-
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34935
|
-
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34936
|
-
* Once that budget is exhausted every further pair answers `true` without being
|
|
34937
|
-
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34938
|
-
* safe side.
|
|
34939
|
-
*/
|
|
34940
|
-
function parsedGlobsIntersect(left, right, budget) {
|
|
34941
|
-
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
34942
|
-
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
34943
|
-
const cost = rows.length * columns.length * cellCost;
|
|
34944
|
-
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
34945
|
-
if (budget !== void 0) {
|
|
34946
|
-
const charge = cost + INTERSECTION_PAIR_COST;
|
|
34947
|
-
if (charge > budget.remaining) {
|
|
34948
|
-
budget.remaining = 0;
|
|
34949
|
-
return true;
|
|
34950
|
-
}
|
|
34951
|
-
budget.remaining -= charge;
|
|
34952
|
-
}
|
|
34953
|
-
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
34954
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
34955
|
-
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
34956
|
-
row[columns.length] = isAllStars(rows, i);
|
|
34957
|
-
for (let j = columns.length - 1; j >= 0; j--) {
|
|
34958
|
-
const rowStep = rows[i];
|
|
34959
|
-
const columnStep = columns[j];
|
|
34960
|
-
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
34961
|
-
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
34962
|
-
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
34963
|
-
continue;
|
|
34964
|
-
}
|
|
34965
|
-
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
34966
|
-
}
|
|
34967
|
-
next = row;
|
|
34968
|
-
}
|
|
34969
|
-
return next[0] ?? false;
|
|
34970
|
-
}
|
|
34971
|
-
//#endregion
|
|
34972
35779
|
//#region src/features/permissions/augmentcode-permissions.ts
|
|
34973
35780
|
const moduleLogger$2 = fallbackLogger;
|
|
34974
35781
|
zod_mini.z.enum([
|
|
@@ -35220,6 +36027,7 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35220
36027
|
const basicExistingEntries = existingEntries.filter((entry) => !isSpecialEntry(entry));
|
|
35221
36028
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
35222
36029
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
36030
|
+
if (entry.toolName === "*") return false;
|
|
35223
36031
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(entry.toolName)) return true;
|
|
35224
36032
|
if (entry.permission.type === "deny") {
|
|
35225
36033
|
const key = `${entry.toolName}|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
@@ -35297,7 +36105,18 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35297
36105
|
};
|
|
35298
36106
|
function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
35299
36107
|
const entries = [];
|
|
35300
|
-
|
|
36108
|
+
const resolvedBashRules = bashRulesHonoringAllTools(config.permission);
|
|
36109
|
+
const permission = config.permission.bash !== void 0 || Object.keys(resolvedBashRules).length > 0 ? {
|
|
36110
|
+
...config.permission,
|
|
36111
|
+
bash: resolvedBashRules
|
|
36112
|
+
} : config.permission;
|
|
36113
|
+
const allToolsFailClosedType = computeAllToolsFailClosedType(config.permission["*"]);
|
|
36114
|
+
const categoriesWithOwnEntries = /* @__PURE__ */ new Set();
|
|
36115
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
36116
|
+
if (category === "*") {
|
|
36117
|
+
logger?.warn("AugmentCode permissions: the all-tools '*' category cannot be emitted as a single entry because AugmentCode has no wildcard toolName. Deny/ask rules are folded into 'launch-process' and, for any other managed tool that has no explicit rules of its own, applied as a blanket deny/ask; all-tools allow rules and tools that already define their own rules are left untouched.");
|
|
36118
|
+
continue;
|
|
36119
|
+
}
|
|
35301
36120
|
const augmentToolName = toAugmentToolName(category);
|
|
35302
36121
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(augmentToolName) && augmentToolName === category) logger?.warn(`AugmentCode permissions: passing through unknown tool category '${category}' as toolName.`);
|
|
35303
36122
|
if (augmentToolName === "launch-process") {
|
|
@@ -35322,19 +36141,50 @@ function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
|
35322
36141
|
toolName: augmentToolName,
|
|
35323
36142
|
permission: { type: "deny" }
|
|
35324
36143
|
});
|
|
36144
|
+
categoriesWithOwnEntries.add(category);
|
|
35325
36145
|
continue;
|
|
35326
36146
|
}
|
|
35327
36147
|
const droppedPatterns = [];
|
|
35328
|
-
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*")
|
|
35329
|
-
|
|
35330
|
-
|
|
35331
|
-
|
|
35332
|
-
|
|
36148
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") {
|
|
36149
|
+
entries.push({
|
|
36150
|
+
toolName: augmentToolName,
|
|
36151
|
+
permission: { type: actionToAugmentType(action) }
|
|
36152
|
+
});
|
|
36153
|
+
categoriesWithOwnEntries.add(category);
|
|
36154
|
+
} else droppedPatterns.push(pattern);
|
|
35333
36155
|
if (droppedPatterns.length > 0) logger?.warn(`AugmentCode permissions: dropping non-wildcard patterns for category '${category}' (${droppedPatterns.join(", ")}); AugmentCode does not document a per-input matcher for this tool. Use a 'deny' rule with pattern '*' if you need to block this tool entirely.`);
|
|
35334
36156
|
}
|
|
36157
|
+
entries.push(...synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType));
|
|
35335
36158
|
return entries;
|
|
35336
36159
|
}
|
|
35337
36160
|
/**
|
|
36161
|
+
* The strictest action the all-tools `*` category imposes, for tools with no per-input matcher to
|
|
36162
|
+
* narrow it onto (see {@link synthesizeManagedToolFallbackEntries}). `deny` wins over `ask`,
|
|
36163
|
+
* and an all-tools `allow` never forces an entry — there is nothing to fail closed on.
|
|
36164
|
+
*/
|
|
36165
|
+
function computeAllToolsFailClosedType(allToolsRules) {
|
|
36166
|
+
if (!allToolsRules) return void 0;
|
|
36167
|
+
const actions = Object.values(allToolsRules);
|
|
36168
|
+
if (actions.some((action) => action === "deny")) return "deny";
|
|
36169
|
+
if (actions.some((action) => action === "ask")) return "ask-user";
|
|
36170
|
+
}
|
|
36171
|
+
/**
|
|
36172
|
+
* Extend the same fail-closed treatment `bash` gets (via {@link bashRulesHonoringAllTools}) to the
|
|
36173
|
+
* other managed tools: one that produced no entries of its own above must not fall back to
|
|
36174
|
+
* AugmentCode's own default just because it has no per-input matcher to narrow the all-tools
|
|
36175
|
+
* restriction onto. A category can be stated yet still emit nothing (e.g. only non-`*` allow/ask
|
|
36176
|
+
* patterns, dropped with a warning), so this checks emitted entries rather than whether the
|
|
36177
|
+
* category was merely present in the source config. A tool whose own rules did emit entries is
|
|
36178
|
+
* left untouched here.
|
|
36179
|
+
*/
|
|
36180
|
+
function synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType) {
|
|
36181
|
+
if (allToolsFailClosedType === void 0) return [];
|
|
36182
|
+
return Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).filter(([canonicalName]) => canonicalName !== "bash" && !categoriesWithOwnEntries.has(canonicalName)).map(([, augmentToolName]) => ({
|
|
36183
|
+
toolName: augmentToolName,
|
|
36184
|
+
permission: { type: allToolsFailClosedType }
|
|
36185
|
+
}));
|
|
36186
|
+
}
|
|
36187
|
+
/**
|
|
35338
36188
|
* Sort AugmentCode tool-permission entries to make the `first-match-wins` semantics safe and predictable.
|
|
35339
36189
|
*
|
|
35340
36190
|
* Augment evaluates `toolPermissions` top-to-bottom and stops at the first match. To prevent a
|
|
@@ -35429,216 +36279,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
35429
36279
|
}
|
|
35430
36280
|
return { permission };
|
|
35431
36281
|
}
|
|
35432
|
-
/**
|
|
35433
|
-
* Collect the canonical rules that govern shell commands, for the adapters
|
|
35434
|
-
* whose tool models commands and nothing else.
|
|
35435
|
-
*
|
|
35436
|
-
* The `bash` category contributes every rule. The all-tools `*` category
|
|
35437
|
-
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
35438
|
-
* written there covers shell commands too, and dropping it inverts the
|
|
35439
|
-
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
35440
|
-
* an adapter that reads only `bash` auto-approves the very command the file
|
|
35441
|
-
* denies.
|
|
35442
|
-
*
|
|
35443
|
-
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35444
|
-
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35445
|
-
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35446
|
-
* it in the permissive direction would grant something the author never said
|
|
35447
|
-
* about commands. Both directions therefore fail closed.
|
|
35448
|
-
*/
|
|
35449
|
-
function collectShellCommandRules(permission) {
|
|
35450
|
-
const rules = [];
|
|
35451
|
-
const foreignRestrictingCategories = [];
|
|
35452
|
-
const ignoredAllToolsAllowPatterns = [];
|
|
35453
|
-
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35454
|
-
if (category === "bash") {
|
|
35455
|
-
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35456
|
-
pattern,
|
|
35457
|
-
action,
|
|
35458
|
-
fromAllToolsCategory: false
|
|
35459
|
-
});
|
|
35460
|
-
continue;
|
|
35461
|
-
}
|
|
35462
|
-
if (category === "*") {
|
|
35463
|
-
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35464
|
-
if (action === "allow") {
|
|
35465
|
-
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35466
|
-
continue;
|
|
35467
|
-
}
|
|
35468
|
-
rules.push({
|
|
35469
|
-
pattern,
|
|
35470
|
-
action,
|
|
35471
|
-
fromAllToolsCategory: true
|
|
35472
|
-
});
|
|
35473
|
-
}
|
|
35474
|
-
continue;
|
|
35475
|
-
}
|
|
35476
|
-
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35477
|
-
}
|
|
35478
|
-
return {
|
|
35479
|
-
rules,
|
|
35480
|
-
foreignRestrictingCategories,
|
|
35481
|
-
ignoredAllToolsAllowPatterns
|
|
35482
|
-
};
|
|
35483
|
-
}
|
|
35484
|
-
/**
|
|
35485
|
-
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35486
|
-
* which restrictions it cannot write name some of the same commands? The
|
|
35487
|
-
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35488
|
-
* written — so a caller can report both the allow rules it withheld and the
|
|
35489
|
-
* restrictions that withheld nothing.
|
|
35490
|
-
*
|
|
35491
|
-
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35492
|
-
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35493
|
-
* asking whether any one command matches both. Width does not enter into it: an
|
|
35494
|
-
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35495
|
-
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35496
|
-
* on every `git ... --force` command even though neither pattern covers the
|
|
35497
|
-
* other's spelling. Comparing only identical spellings would let the most
|
|
35498
|
-
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35499
|
-
*
|
|
35500
|
-
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35501
|
-
* the walk for the commonest case.
|
|
35502
|
-
*
|
|
35503
|
-
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35504
|
-
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35505
|
-
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35506
|
-
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35507
|
-
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35508
|
-
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35509
|
-
* withholds an allow rather than writing one the config restricts — see
|
|
35510
|
-
* `warpCommandPatternToGlob`.
|
|
35511
|
-
*/
|
|
35512
|
-
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35513
|
-
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35514
|
-
pattern,
|
|
35515
|
-
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35516
|
-
}));
|
|
35517
|
-
return (allowPattern) => {
|
|
35518
|
-
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35519
|
-
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35520
|
-
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35521
|
-
};
|
|
35522
|
-
}
|
|
35523
|
-
/**
|
|
35524
|
-
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35525
|
-
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35526
|
-
*
|
|
35527
|
-
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35528
|
-
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35529
|
-
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35530
|
-
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35531
|
-
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35532
|
-
*
|
|
35533
|
-
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35534
|
-
* so overlapping no allow rule says nothing is wrong with it.
|
|
35535
|
-
*/
|
|
35536
|
-
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35537
|
-
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35538
|
-
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35539
|
-
return (0, es_toolkit.uniq)(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35540
|
-
}
|
|
35541
|
-
/**
|
|
35542
|
-
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35543
|
-
* commands with those two tiers and nothing else.
|
|
35544
|
-
*
|
|
35545
|
-
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35546
|
-
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35547
|
-
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35548
|
-
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35549
|
-
* about would answer the prompt the author wanted.
|
|
35550
|
-
*
|
|
35551
|
-
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35552
|
-
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35553
|
-
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35554
|
-
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35555
|
-
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35556
|
-
* cannot be written it withholds the allow rules it covers instead, which
|
|
35557
|
-
* restricts in the same direction without touching the denylist.
|
|
35558
|
-
*
|
|
35559
|
-
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35560
|
-
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35561
|
-
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35562
|
-
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35563
|
-
* even where it is written: a pattern under `*` need not name a command —
|
|
35564
|
-
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35565
|
-
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35566
|
-
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35567
|
-
* command pattern is reported; failing open would not be.
|
|
35568
|
-
*
|
|
35569
|
-
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35570
|
-
* patterns are not globs.
|
|
35571
|
-
*/
|
|
35572
|
-
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35573
|
-
const deny = [];
|
|
35574
|
-
const unwrittenDenyPatterns = [];
|
|
35575
|
-
const restrictions = [];
|
|
35576
|
-
const writtenAllToolsDenyPatterns = [];
|
|
35577
|
-
const allToolsAskPatterns = [];
|
|
35578
|
-
for (const rule of rules) {
|
|
35579
|
-
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35580
|
-
if (action === "allow") continue;
|
|
35581
|
-
if (action !== "deny") {
|
|
35582
|
-
restrictions.push(rule);
|
|
35583
|
-
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35584
|
-
continue;
|
|
35585
|
-
}
|
|
35586
|
-
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35587
|
-
deny.push(pattern);
|
|
35588
|
-
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35589
|
-
} else unwrittenDenyPatterns.push(pattern);
|
|
35590
|
-
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35591
|
-
}
|
|
35592
|
-
const budget = createIntersectionBudget();
|
|
35593
|
-
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35594
|
-
normalizePattern,
|
|
35595
|
-
budget
|
|
35596
|
-
});
|
|
35597
|
-
const allow = [];
|
|
35598
|
-
const shadowedAllowPatterns = [];
|
|
35599
|
-
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35600
|
-
for (const { pattern, action } of rules) {
|
|
35601
|
-
if (action !== "allow") continue;
|
|
35602
|
-
const shadowing = shadowingRestrictions(pattern);
|
|
35603
|
-
if (shadowing.length > 0) {
|
|
35604
|
-
shadowedAllowPatterns.push(pattern);
|
|
35605
|
-
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35606
|
-
continue;
|
|
35607
|
-
}
|
|
35608
|
-
allow.push(pattern);
|
|
35609
|
-
}
|
|
35610
|
-
return {
|
|
35611
|
-
allow,
|
|
35612
|
-
deny,
|
|
35613
|
-
shadowedAllowPatterns,
|
|
35614
|
-
unwrittenDenyPatterns,
|
|
35615
|
-
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35616
|
-
rules,
|
|
35617
|
-
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35618
|
-
withholdingPatterns
|
|
35619
|
-
}),
|
|
35620
|
-
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35621
|
-
rules,
|
|
35622
|
-
allToolsPatterns: allToolsAskPatterns,
|
|
35623
|
-
withholdingPatterns
|
|
35624
|
-
}),
|
|
35625
|
-
intersectionBudgetExhausted: budget.remaining === 0
|
|
35626
|
-
};
|
|
35627
|
-
}
|
|
35628
|
-
/**
|
|
35629
|
-
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35630
|
-
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35631
|
-
* dropped in one is worded the same way in all.
|
|
35632
|
-
*/
|
|
35633
|
-
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35634
|
-
if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
|
|
35635
|
-
for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
|
|
35636
|
-
if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
|
|
35637
|
-
if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
|
|
35638
|
-
if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
|
|
35639
|
-
if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
|
|
35640
|
-
if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
|
|
35641
|
-
}
|
|
35642
36282
|
//#endregion
|
|
35643
36283
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
35644
36284
|
/**
|
|
@@ -37400,7 +38040,7 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
|
|
|
37400
38040
|
return merged;
|
|
37401
38041
|
}
|
|
37402
38042
|
function buildCodexBashRulesContent(config) {
|
|
37403
|
-
const bashRules = config.permission
|
|
38043
|
+
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
37404
38044
|
const entries = Object.entries(bashRules);
|
|
37405
38045
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
37406
38046
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
@@ -38091,7 +38731,7 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
38091
38731
|
function convertRulesyncToCursorPermissions(config, logger) {
|
|
38092
38732
|
const allow = [];
|
|
38093
38733
|
const deny = [];
|
|
38094
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
38734
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38095
38735
|
const cursorType = toCursorType(category);
|
|
38096
38736
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38097
38737
|
const entry = buildCursorPermissionEntry(cursorType, toCursorPattern(category, pattern));
|
|
@@ -38910,7 +39550,7 @@ function convertRulesyncToDevinPermissions(config) {
|
|
|
38910
39550
|
const allow = [];
|
|
38911
39551
|
const ask = [];
|
|
38912
39552
|
const deny = [];
|
|
38913
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
39553
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38914
39554
|
const scope = toDevinScope(category);
|
|
38915
39555
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38916
39556
|
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
@@ -39198,6 +39838,19 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
39198
39838
|
isDeletable() {
|
|
39199
39839
|
return false;
|
|
39200
39840
|
}
|
|
39841
|
+
/**
|
|
39842
|
+
* `permission.yaml` is Goose's file, not one rulesync owns: rulesync merges
|
|
39843
|
+
* into it when it exists but has no business bringing it into existence to
|
|
39844
|
+
* hold nothing. When no rule maps, the `user` block holds three empty lists,
|
|
39845
|
+
* which would otherwise be written as a fresh permission.yaml that says
|
|
39846
|
+
* nothing — an absent file and empty lists both mean "no user override, so
|
|
39847
|
+
* Goose decides on its own". An existing file is still rewritten as before,
|
|
39848
|
+
* so user content is never dropped — the skip only applies when there is no
|
|
39849
|
+
* file yet.
|
|
39850
|
+
*/
|
|
39851
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
39852
|
+
return true;
|
|
39853
|
+
}
|
|
39201
39854
|
static getSettablePaths(_options) {
|
|
39202
39855
|
return {
|
|
39203
39856
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
@@ -39282,7 +39935,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
39282
39935
|
never_allow: []
|
|
39283
39936
|
};
|
|
39284
39937
|
const assigned = /* @__PURE__ */ new Map();
|
|
39285
|
-
const orderedEntries = Object.entries(config.permission).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39938
|
+
const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39286
39939
|
for (const [category, rules] of orderedEntries) {
|
|
39287
39940
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
39288
39941
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -39614,7 +40267,7 @@ function unmanagedEntries(existingPermission, key) {
|
|
|
39614
40267
|
*/
|
|
39615
40268
|
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
39616
40269
|
const ranked = /* @__PURE__ */ new Map();
|
|
39617
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
40270
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
39618
40271
|
const entry = buildGrokEntry(category, pattern);
|
|
39619
40272
|
if (entry === null) {
|
|
39620
40273
|
if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
|
|
@@ -39703,6 +40356,8 @@ function deriveGrokPermissionMode(config) {
|
|
|
39703
40356
|
function patternsByAction(category, action) {
|
|
39704
40357
|
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
39705
40358
|
}
|
|
40359
|
+
/** The canonical category whose `deny` rules feed `security.website_blocklist`. */
|
|
40360
|
+
const WEBFETCH_PERMISSION_CATEGORY = "webfetch";
|
|
39706
40361
|
function clonePermissionBlock(permission) {
|
|
39707
40362
|
return Object.fromEntries(Object.entries(permission).map(([category, rules]) => [category, { ...rules }]));
|
|
39708
40363
|
}
|
|
@@ -39715,18 +40370,39 @@ function ensureCategory(permission, category) {
|
|
|
39715
40370
|
function removeEmptyCategories(permission) {
|
|
39716
40371
|
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
39717
40372
|
}
|
|
40373
|
+
/**
|
|
40374
|
+
* Make the native `command_allowlist` authoritative for the `bash` allow rules
|
|
40375
|
+
* it can speak for, and for those only. The allowlist is a list of shell-command
|
|
40376
|
+
* patterns, so it is generated from `bash` alone (see `fromRulesyncPermissions`);
|
|
40377
|
+
* an allow in any other category names something Hermes's allowlist cannot
|
|
40378
|
+
* carry, so its absence from the list says nothing about it and it is kept as
|
|
40379
|
+
* provenance wrote it. The same holds for a `bash` allow the generator withheld
|
|
40380
|
+
* because a stricter `*` or `bash` rule covers it: the allowlist never carried
|
|
40381
|
+
* it, so its absence is not a retraction and the rule is kept too.
|
|
40382
|
+
*/
|
|
39718
40383
|
function reconcileCommandAllowlist({ permission, commandAllowlist }) {
|
|
39719
|
-
const
|
|
39720
|
-
const
|
|
39721
|
-
|
|
39722
|
-
|
|
39723
|
-
|
|
39724
|
-
|
|
39725
|
-
|
|
39726
|
-
for (const pattern of
|
|
39727
|
-
|
|
39728
|
-
|
|
39729
|
-
|
|
40384
|
+
const { rules: commandRules } = collectShellCommandRules(permission);
|
|
40385
|
+
const { shadowedAllowPatterns } = partitionCommandRules({
|
|
40386
|
+
rules: commandRules,
|
|
40387
|
+
writesAllToolsDeny: false
|
|
40388
|
+
});
|
|
40389
|
+
const withheld = new Set(shadowedAllowPatterns);
|
|
40390
|
+
const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
|
|
40391
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
|
|
40392
|
+
for (const pattern of commandAllowlist) rules[pattern] = "allow";
|
|
40393
|
+
}
|
|
40394
|
+
/**
|
|
40395
|
+
* Report the restricting rules Hermes has no per-pattern primitive for: a
|
|
40396
|
+
* `deny` or `ask` in any category other than `bash`, `*`, and `webfetch`, and
|
|
40397
|
+
* an `ask` under `webfetch` — the blocklist carries a `webfetch` deny but has
|
|
40398
|
+
* no ask tier. (`bash` and `*` are reported by `warnAboutUnwrittenCommandRules`.)
|
|
40399
|
+
* Such rules survive only in the round-trip blob.
|
|
40400
|
+
*/
|
|
40401
|
+
function warnAboutUnexpressedHermesRestrictions({ permissionBlock, foreignRestrictingCategories, logger }) {
|
|
40402
|
+
for (const category of foreignRestrictingCategories) {
|
|
40403
|
+
const isWebfetch = category === WEBFETCH_PERMISSION_CATEGORY;
|
|
40404
|
+
if (isWebfetch && patternsByAction(permissionBlock[category], "ask").length === 0) continue;
|
|
40405
|
+
warnWithFallback(logger, isWebfetch ? "Hermes Agent's security.website_blocklist has no ask tier, so the 'webfetch' ask rule(s) cannot be represented and were skipped; they survive only in the permissions.rulesync round-trip block." : `Hermes Agent has no per-pattern primitive for '${category}' deny and ask rules (it enforces command_allowlist, approvals.deny, and security.website_blocklist), so they were skipped; they survive only in the permissions.rulesync round-trip block.`);
|
|
39730
40406
|
}
|
|
39731
40407
|
}
|
|
39732
40408
|
function reconcileNativeDenies({ permission, category, patterns }) {
|
|
@@ -39838,14 +40514,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39838
40514
|
const approvals = isRecord$1(config.approvals) ? config.approvals : {};
|
|
39839
40515
|
reconcileNativeDenies({
|
|
39840
40516
|
permission,
|
|
39841
|
-
category:
|
|
40517
|
+
category: SHELL_PERMISSION_CATEGORY,
|
|
39842
40518
|
patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
|
|
39843
40519
|
});
|
|
39844
40520
|
const security = isRecord$1(config.security) ? config.security : {};
|
|
39845
40521
|
const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
|
|
39846
40522
|
reconcileNativeDenies({
|
|
39847
40523
|
permission,
|
|
39848
|
-
category:
|
|
40524
|
+
category: WEBFETCH_PERMISSION_CATEGORY,
|
|
39849
40525
|
patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
|
|
39850
40526
|
});
|
|
39851
40527
|
removeEmptyCategories(permission);
|
|
@@ -39865,12 +40541,32 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39865
40541
|
fileContent: JSON.stringify(imported, null, 2)
|
|
39866
40542
|
});
|
|
39867
40543
|
}
|
|
39868
|
-
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
|
|
40544
|
+
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false, logger }) {
|
|
39869
40545
|
const permissions = rulesyncPermissions.getJson();
|
|
39870
40546
|
const permissionBlock = permissions.permission ?? {};
|
|
39871
|
-
const
|
|
39872
|
-
const bashDeny =
|
|
39873
|
-
|
|
40547
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permissionBlock);
|
|
40548
|
+
const { allow: commandAllowlist, deny: bashDeny, shadowedAllowPatterns, unwrittenDenyPatterns, unenforcedAllToolsAskPatterns, intersectionBudgetExhausted } = partitionCommandRules({
|
|
40549
|
+
rules,
|
|
40550
|
+
writesAllToolsDeny: false
|
|
40551
|
+
});
|
|
40552
|
+
warnAboutUnexpressedHermesRestrictions({
|
|
40553
|
+
permissionBlock,
|
|
40554
|
+
foreignRestrictingCategories,
|
|
40555
|
+
logger
|
|
40556
|
+
});
|
|
40557
|
+
warnAboutUnwrittenCommandRules({
|
|
40558
|
+
toolLabel: "Hermes Agent",
|
|
40559
|
+
surfaceLabel: "command_allowlist/approvals.deny",
|
|
40560
|
+
foreignRestrictingCategories: [],
|
|
40561
|
+
shadowedAllowPatterns,
|
|
40562
|
+
unwrittenDenyPatterns,
|
|
40563
|
+
unwrittenDenyReason: "approvals.deny is a hard denylist of shell commands, and a pattern written under '*' need not be a command at all.",
|
|
40564
|
+
unenforcedAllToolsAskPatterns,
|
|
40565
|
+
ignoredAllToolsAllowPatterns,
|
|
40566
|
+
intersectionBudgetExhausted,
|
|
40567
|
+
logger
|
|
40568
|
+
});
|
|
40569
|
+
const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
|
|
39874
40570
|
let config = {};
|
|
39875
40571
|
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
39876
40572
|
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
@@ -40137,7 +40833,7 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
40137
40833
|
*/
|
|
40138
40834
|
function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
|
|
40139
40835
|
const ruleLists = {};
|
|
40140
|
-
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
40836
|
+
for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
40141
40837
|
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
40142
40838
|
if (!group) {
|
|
40143
40839
|
if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
|
|
@@ -40387,7 +41083,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
40387
41083
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
40388
41084
|
const kiloOverride = rulesyncJson.kilo;
|
|
40389
41085
|
const incomingPermission = {
|
|
40390
|
-
...rulesyncJson.permission,
|
|
41086
|
+
...honorAllToolsOnBash(rulesyncJson.permission),
|
|
40391
41087
|
...kiloOverride?.permission
|
|
40392
41088
|
};
|
|
40393
41089
|
const droppedDenyByKey = {};
|
|
@@ -40906,7 +41602,7 @@ function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
|
40906
41602
|
allowedCommands: [],
|
|
40907
41603
|
deniedCommands: []
|
|
40908
41604
|
};
|
|
40909
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
41605
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
40910
41606
|
if (action === "ask") {
|
|
40911
41607
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
40912
41608
|
continue;
|
|
@@ -41190,7 +41886,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
41190
41886
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
41191
41887
|
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
41192
41888
|
const sharedPermission = {};
|
|
41193
|
-
for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41889
|
+
for (const [category, value] of Object.entries(honorAllToolsOnBash(rulesyncJson.permission ?? {}))) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41194
41890
|
const permission = {};
|
|
41195
41891
|
for (const [category, value] of Object.entries({
|
|
41196
41892
|
...sharedPermission,
|
|
@@ -42319,11 +43015,19 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42319
43015
|
const paths = this.getSettablePaths();
|
|
42320
43016
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
42321
43017
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
42322
|
-
const
|
|
43018
|
+
const permission = rulesyncPermissions.getJson().permission;
|
|
43019
|
+
const bashStated = permission[COMMAND_CATEGORY] !== void 0;
|
|
42323
43020
|
const patch = {};
|
|
42324
|
-
if (
|
|
43021
|
+
if (bashStated) {
|
|
43022
|
+
const { bash } = resolveShellCommandLists({
|
|
43023
|
+
permission,
|
|
43024
|
+
writesAllToolsDeny: true,
|
|
43025
|
+
toolLabel: this.getToolLabel(),
|
|
43026
|
+
surfaceLabel: `${this.getAllowedCommandsKey()}/${this.getDeniedCommandsKey()}`,
|
|
43027
|
+
logger
|
|
43028
|
+
});
|
|
42325
43029
|
const { allowed, denied } = buildVscodeCommandLists({
|
|
42326
|
-
rules,
|
|
43030
|
+
rules: bash,
|
|
42327
43031
|
toolLabel: this.getToolLabel(),
|
|
42328
43032
|
logger
|
|
42329
43033
|
});
|
|
@@ -42334,7 +43038,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42334
43038
|
outputRoot,
|
|
42335
43039
|
relativeDirPath: paths.relativeDirPath,
|
|
42336
43040
|
relativeFilePath: paths.relativeFilePath,
|
|
42337
|
-
ownsCommandKeys:
|
|
43041
|
+
ownsCommandKeys: bashStated,
|
|
42338
43042
|
fileContent: applySharedConfigPatch({
|
|
42339
43043
|
fileKey: sharedConfigFileKey(paths),
|
|
42340
43044
|
feature: "permissions",
|
|
@@ -42714,7 +43418,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
42714
43418
|
config,
|
|
42715
43419
|
logger
|
|
42716
43420
|
});
|
|
42717
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
43421
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42718
43422
|
if (category === CATCH_ALL_PATTERN$1) {
|
|
42719
43423
|
const toolWideDefault = convertAllToolsRules({
|
|
42720
43424
|
rules,
|
|
@@ -43724,7 +44428,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
43724
44428
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43725
44429
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
43726
44430
|
const config = parseVibeConfig(existingContent);
|
|
43727
|
-
const permission = rulesyncPermissions.getJson().permission;
|
|
44431
|
+
const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
|
|
43728
44432
|
const vibeOverride = rulesyncPermissions.getJson().vibe;
|
|
43729
44433
|
const tools = toVibeToolsRecord(config.tools);
|
|
43730
44434
|
const diskShellPatterns = new Map([VIBE_SHELL_CATEGORY, ...VIBE_SHELL_ALIAS_TOOL_NAMES].map((vibeToolName) => [vibeToolName, toStringArray(readVibeToolConfig({
|
|
@@ -45065,6 +45769,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
45065
45769
|
isDeletable() {
|
|
45066
45770
|
return false;
|
|
45067
45771
|
}
|
|
45772
|
+
/**
|
|
45773
|
+
* `settings.toml` is Warp's file, not one rulesync owns: rulesync merges into
|
|
45774
|
+
* it when it exists but has no business bringing it into existence to hold
|
|
45775
|
+
* nothing. When no rule maps, both command lists are dropped and the payload
|
|
45776
|
+
* is a bare `[agents.profiles]` table, which would otherwise be written as a
|
|
45777
|
+
* fresh settings file that says nothing — an absent file and an empty table
|
|
45778
|
+
* both mean "Warp's own defaults". An existing file is still rewritten as
|
|
45779
|
+
* before, so user content is never dropped — the skip only applies when
|
|
45780
|
+
* there is no file yet.
|
|
45781
|
+
*/
|
|
45782
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
45783
|
+
return true;
|
|
45784
|
+
}
|
|
45068
45785
|
static getSettablePaths(_options) {
|
|
45069
45786
|
return {
|
|
45070
45787
|
relativeDirPath: warpSettingsDir(),
|
|
@@ -45676,6 +46393,7 @@ function buildZedToolPermissions({ permission, logger }) {
|
|
|
45676
46393
|
for (const [category, rules] of Object.entries(permission)) {
|
|
45677
46394
|
if (category === "*") {
|
|
45678
46395
|
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
46396
|
+
else if (permission.bash?.[pattern] === "deny" || permission.bash?.[pattern] === "ask") continue;
|
|
45679
46397
|
else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
|
|
45680
46398
|
continue;
|
|
45681
46399
|
}
|
|
@@ -45830,7 +46548,7 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
45830
46548
|
const toolPermissions = asRecord(agent.tool_permissions);
|
|
45831
46549
|
const existingTools = asRecord(toolPermissions.tools);
|
|
45832
46550
|
const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
|
|
45833
|
-
permission: config.permission,
|
|
46551
|
+
permission: honorAllToolsOnBash(config.permission),
|
|
45834
46552
|
logger
|
|
45835
46553
|
});
|
|
45836
46554
|
if (excludedCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${excludedCategories.map((category) => `"${category}"`).join(", ")} ${excludedCategories.length === 1 ? "category" : "categories"} — Zed does not gate its read-only tools, so the entries would never be consulted. Zed's read-denial surface is \`private_files\`, which the ignore feature writes from \`.rulesync/.aiignore\`.`);
|
|
@@ -46339,6 +47057,24 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
46339
47057
|
}
|
|
46340
47058
|
};
|
|
46341
47059
|
//#endregion
|
|
47060
|
+
//#region src/constants/codebuddy-paths.ts
|
|
47061
|
+
/**
|
|
47062
|
+
* CodeBuddy Code configuration-layout conventions.
|
|
47063
|
+
*
|
|
47064
|
+
* CodeBuddy Code (`@tencent-ai/codebuddy-code`) is Tencent Cloud's terminal
|
|
47065
|
+
* coding agent. Its configuration surface mirrors Claude Code closely: a
|
|
47066
|
+
* root memory file plus a `.codebuddy/` tree.
|
|
47067
|
+
*
|
|
47068
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
47069
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
47070
|
+
*/
|
|
47071
|
+
/** Root directory for CodeBuddy Code configuration, relative to the scope root. */
|
|
47072
|
+
const CODEBUDDY_DIR = ".codebuddy";
|
|
47073
|
+
const CODEBUDDY_RULE_FILE_NAME = "CODEBUDDY.md";
|
|
47074
|
+
const CODEBUDDY_LOCAL_RULE_FILE_NAME = "CODEBUDDY.local.md";
|
|
47075
|
+
/** Modular rules directory name under `.codebuddy/`. */
|
|
47076
|
+
const CODEBUDDY_RULES_DIR_NAME = "rules";
|
|
47077
|
+
//#endregion
|
|
46342
47078
|
//#region src/features/skills/simulated-skill.ts
|
|
46343
47079
|
const SimulatedSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
46344
47080
|
name: zod_mini.z.string(),
|
|
@@ -46877,7 +47613,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46877
47613
|
* This only deletes directories that are no longer in the rulesync source, not directories that will be overwritten.
|
|
46878
47614
|
*/
|
|
46879
47615
|
async removeOrphanAiDirs(existingDirs, generatedDirs) {
|
|
46880
|
-
const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
|
|
47616
|
+
const generatedPaths = new Set(generatedDirs.map((d) => caseFoldIdentity(d.getDirPath())));
|
|
46881
47617
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46882
47618
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46883
47619
|
for (const aiDir of existingDirs) {
|
|
@@ -46902,7 +47638,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46902
47638
|
this.logger.warn(verdict === "equal" ? `Refusing to delete ${quotedDirPath}: it is the root it was found in, not a directory inside that root` : `Refusing to delete ${quotedDirPath}: it is not inside ${quotedRoot}, the root it was found in`);
|
|
46903
47639
|
continue;
|
|
46904
47640
|
}
|
|
46905
|
-
if (!generatedPaths.has(dirPath)) orphanPaths.add(dirPath);
|
|
47641
|
+
if (!generatedPaths.has(caseFoldIdentity(dirPath))) orphanPaths.add(dirPath);
|
|
46906
47642
|
}
|
|
46907
47643
|
return await this.deleteOrphanPaths({
|
|
46908
47644
|
paths: orphanPaths,
|
|
@@ -47001,7 +47737,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47001
47737
|
const mainFile = aiDir.getMainFile();
|
|
47002
47738
|
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
47003
47739
|
for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
|
|
47004
|
-
const generatedNamesFolded = new Set([...generatedNames].map((name) => name
|
|
47740
|
+
const generatedNamesFolded = new Set([...generatedNames].map((name) => caseFoldIdentity(name)));
|
|
47005
47741
|
let existingNames;
|
|
47006
47742
|
try {
|
|
47007
47743
|
existingNames = await listFilePathsRecursively(dirPath, {
|
|
@@ -47016,7 +47752,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47016
47752
|
const posixName = toPosixPath(existingName);
|
|
47017
47753
|
if (generatedNames.has(posixName)) continue;
|
|
47018
47754
|
const filePath = (0, node_path.join)(dirPath, existingName);
|
|
47019
|
-
if (generatedNamesFolded.has(posixName
|
|
47755
|
+
if (generatedNamesFolded.has(caseFoldIdentity(posixName))) {
|
|
47020
47756
|
this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
47021
47757
|
continue;
|
|
47022
47758
|
}
|
|
@@ -47072,7 +47808,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47072
47808
|
generatedPaths.add(flatFilePath);
|
|
47073
47809
|
for (const file of generatedDir.getOtherFiles()) generatedPaths.add((0, node_path.join)(generatedDirPath, file.relativeFilePathToDirPath));
|
|
47074
47810
|
}
|
|
47075
|
-
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath
|
|
47811
|
+
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => caseFoldIdentity(generatedPath)));
|
|
47076
47812
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
47077
47813
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
47078
47814
|
for (const aiDir of existingFlatFiles) {
|
|
@@ -47102,7 +47838,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47102
47838
|
continue;
|
|
47103
47839
|
}
|
|
47104
47840
|
if (generatedPaths.has(filePath)) continue;
|
|
47105
|
-
if (generatedPathsFolded.has(filePath
|
|
47841
|
+
if (generatedPathsFolded.has(caseFoldIdentity(filePath))) {
|
|
47106
47842
|
this.logger.warn(`Refusing to delete ${quotedFilePath}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
47107
47843
|
continue;
|
|
47108
47844
|
}
|
|
@@ -48934,6 +49670,191 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
48934
49670
|
}
|
|
48935
49671
|
};
|
|
48936
49672
|
//#endregion
|
|
49673
|
+
//#region src/features/skills/crush-skill.ts
|
|
49674
|
+
const CrushSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
49675
|
+
name: zod_mini.z.string(),
|
|
49676
|
+
description: zod_mini.z.string(),
|
|
49677
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
49678
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
49679
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
49680
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
49681
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
49682
|
+
});
|
|
49683
|
+
/**
|
|
49684
|
+
* Represents a Crush Agent Skill directory.
|
|
49685
|
+
*
|
|
49686
|
+
* Crush auto-discovers Agent Skills (`SKILL.md` per directory) from
|
|
49687
|
+
* `.crush/skills/` at project scope and `~/.config/crush/skills/` (or
|
|
49688
|
+
* `$CRUSH_SKILLS_DIR`) at global scope. Unless `$CRUSH_SKILLS_DIR` is set,
|
|
49689
|
+
* Crush also scans several shared directories it does not own (globally
|
|
49690
|
+
* `~/.config/agents/skills/`, `~/.agents/skills/`, `~/.claude/skills/`;
|
|
49691
|
+
* per-project `.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, also
|
|
49692
|
+
* checked at a git worktree's common root); this class writes only to the
|
|
49693
|
+
* Crush-specific path above, leaving those shared roots to their own targets.
|
|
49694
|
+
*
|
|
49695
|
+
* Crush's `UserInvocable` field is a non-pointer Go `bool`, so an omitted
|
|
49696
|
+
* `user-invocable` (at both the root and the `crush:` section) resolves to
|
|
49697
|
+
* `false`: the skill stays reachable by the model but is hidden from Crush's
|
|
49698
|
+
* command palette. See `FromSkillCatalog` in `internal/commands/commands.go`.
|
|
49699
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
49700
|
+
*/
|
|
49701
|
+
var CrushSkill = class CrushSkill extends ToolSkill {
|
|
49702
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = CRUSH_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
49703
|
+
super({
|
|
49704
|
+
outputRoot,
|
|
49705
|
+
relativeDirPath,
|
|
49706
|
+
dirName,
|
|
49707
|
+
mainFile: {
|
|
49708
|
+
name: SKILL_FILE_NAME,
|
|
49709
|
+
body,
|
|
49710
|
+
frontmatter: { ...frontmatter }
|
|
49711
|
+
},
|
|
49712
|
+
otherFiles,
|
|
49713
|
+
global
|
|
49714
|
+
});
|
|
49715
|
+
if (validate) {
|
|
49716
|
+
const result = this.validate();
|
|
49717
|
+
if (!result.success) throw result.error;
|
|
49718
|
+
}
|
|
49719
|
+
}
|
|
49720
|
+
static getSettablePaths({ global = false } = {}) {
|
|
49721
|
+
return { relativeDirPath: global ? CRUSH_SKILLS_GLOBAL_DIR : CRUSH_SKILLS_PROJECT_DIR };
|
|
49722
|
+
}
|
|
49723
|
+
getFrontmatter() {
|
|
49724
|
+
return CrushSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
49725
|
+
}
|
|
49726
|
+
getBody() {
|
|
49727
|
+
return this.mainFile?.body ?? "";
|
|
49728
|
+
}
|
|
49729
|
+
validate() {
|
|
49730
|
+
if (!this.mainFile) return {
|
|
49731
|
+
success: false,
|
|
49732
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
49733
|
+
};
|
|
49734
|
+
const result = CrushSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
49735
|
+
if (!result.success) return {
|
|
49736
|
+
success: false,
|
|
49737
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
49738
|
+
};
|
|
49739
|
+
return {
|
|
49740
|
+
success: true,
|
|
49741
|
+
error: null
|
|
49742
|
+
};
|
|
49743
|
+
}
|
|
49744
|
+
toRulesyncSkill() {
|
|
49745
|
+
const frontmatter = this.getFrontmatter();
|
|
49746
|
+
const crushSection = {
|
|
49747
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
49748
|
+
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
49749
|
+
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
49750
|
+
...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
|
|
49751
|
+
...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
|
|
49752
|
+
};
|
|
49753
|
+
const rulesyncFrontmatter = {
|
|
49754
|
+
name: frontmatter.name,
|
|
49755
|
+
description: frontmatter.description,
|
|
49756
|
+
targets: ["*"],
|
|
49757
|
+
...Object.keys(crushSection).length > 0 && { crush: crushSection }
|
|
49758
|
+
};
|
|
49759
|
+
return new RulesyncSkill({
|
|
49760
|
+
outputRoot: this.outputRoot,
|
|
49761
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
49762
|
+
dirName: this.getDirName(),
|
|
49763
|
+
frontmatter: rulesyncFrontmatter,
|
|
49764
|
+
body: this.getBody(),
|
|
49765
|
+
otherFiles: this.getOtherFiles(),
|
|
49766
|
+
validate: true,
|
|
49767
|
+
global: this.global
|
|
49768
|
+
});
|
|
49769
|
+
}
|
|
49770
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49771
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49772
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49773
|
+
const crushSection = rulesyncFrontmatter.crush;
|
|
49774
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
49775
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49776
|
+
section: crushSection
|
|
49777
|
+
});
|
|
49778
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
49779
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49780
|
+
section: crushSection
|
|
49781
|
+
});
|
|
49782
|
+
const license = resolveLicense({
|
|
49783
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49784
|
+
section: crushSection
|
|
49785
|
+
});
|
|
49786
|
+
const compatibility = resolveCompatibility({
|
|
49787
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49788
|
+
section: crushSection
|
|
49789
|
+
});
|
|
49790
|
+
const metadata = resolveMetadata({
|
|
49791
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49792
|
+
section: crushSection
|
|
49793
|
+
});
|
|
49794
|
+
const compatibilityString = compatibility === void 0 ? void 0 : toCompatibilityString(compatibility);
|
|
49795
|
+
const crushFrontmatter = {
|
|
49796
|
+
name: rulesyncFrontmatter.name,
|
|
49797
|
+
description: rulesyncFrontmatter.description,
|
|
49798
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
49799
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
49800
|
+
...license !== void 0 && { license },
|
|
49801
|
+
...compatibilityString !== void 0 && compatibilityString.length > 0 && { compatibility: compatibilityString },
|
|
49802
|
+
...metadata !== void 0 && { metadata: toStringMetadata(metadata) }
|
|
49803
|
+
};
|
|
49804
|
+
return new CrushSkill({
|
|
49805
|
+
outputRoot,
|
|
49806
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
49807
|
+
dirName: rulesyncSkill.getDirName(),
|
|
49808
|
+
frontmatter: crushFrontmatter,
|
|
49809
|
+
body: rulesyncSkill.getBody(),
|
|
49810
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
49811
|
+
validate,
|
|
49812
|
+
global
|
|
49813
|
+
});
|
|
49814
|
+
}
|
|
49815
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
49816
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
49817
|
+
return targets.includes("*") || targets.includes("crush");
|
|
49818
|
+
}
|
|
49819
|
+
static async fromDir(params) {
|
|
49820
|
+
const loaded = await this.loadSkillDirContent({
|
|
49821
|
+
...params,
|
|
49822
|
+
getSettablePaths: CrushSkill.getSettablePaths
|
|
49823
|
+
});
|
|
49824
|
+
const result = CrushSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
49825
|
+
if (!result.success) {
|
|
49826
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
49827
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
49828
|
+
}
|
|
49829
|
+
return new CrushSkill({
|
|
49830
|
+
outputRoot: loaded.outputRoot,
|
|
49831
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
49832
|
+
dirName: loaded.dirName,
|
|
49833
|
+
frontmatter: result.data,
|
|
49834
|
+
body: loaded.body,
|
|
49835
|
+
otherFiles: loaded.otherFiles,
|
|
49836
|
+
validate: true,
|
|
49837
|
+
global: loaded.global
|
|
49838
|
+
});
|
|
49839
|
+
}
|
|
49840
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
49841
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49842
|
+
return new CrushSkill({
|
|
49843
|
+
outputRoot,
|
|
49844
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
49845
|
+
dirName,
|
|
49846
|
+
frontmatter: {
|
|
49847
|
+
name: "",
|
|
49848
|
+
description: ""
|
|
49849
|
+
},
|
|
49850
|
+
body: "",
|
|
49851
|
+
otherFiles: [],
|
|
49852
|
+
validate: false,
|
|
49853
|
+
global
|
|
49854
|
+
});
|
|
49855
|
+
}
|
|
49856
|
+
};
|
|
49857
|
+
//#endregion
|
|
48937
49858
|
//#region src/features/skills/cursor-skill.ts
|
|
48938
49859
|
const CursorSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
48939
49860
|
name: zod_mini.z.string(),
|
|
@@ -49098,9 +50019,9 @@ const DeepagentsSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
49098
50019
|
name: zod_mini.z.string(),
|
|
49099
50020
|
description: zod_mini.z.string(),
|
|
49100
50021
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
49101
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
49102
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
49103
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
50022
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50023
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50024
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
49104
50025
|
});
|
|
49105
50026
|
var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
|
|
49106
50027
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DEEPAGENTS_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -50158,9 +51079,9 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
50158
51079
|
const KiloSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
50159
51080
|
name: zod_mini.z.string(),
|
|
50160
51081
|
description: zod_mini.z.string(),
|
|
50161
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
50162
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
50163
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
51082
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51083
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51084
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50164
51085
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
50165
51086
|
});
|
|
50166
51087
|
var KiloSkill = class KiloSkill extends ToolSkill {
|
|
@@ -50597,10 +51518,11 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
50597
51518
|
rootFrontmatter: rulesyncFrontmatter,
|
|
50598
51519
|
section: kiroSection
|
|
50599
51520
|
});
|
|
51521
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = kiroSection ?? {};
|
|
50600
51522
|
const kiroFrontmatter = {
|
|
50601
|
-
...kiroSection,
|
|
50602
51523
|
name: rulesyncFrontmatter.name,
|
|
50603
51524
|
description: rulesyncFrontmatter.description,
|
|
51525
|
+
...section,
|
|
50604
51526
|
...license !== void 0 && { license },
|
|
50605
51527
|
...compatibility !== void 0 && { compatibility },
|
|
50606
51528
|
...metadata !== void 0 && { metadata }
|
|
@@ -50832,9 +51754,9 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
|
50832
51754
|
const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
50833
51755
|
name: zod_mini.z.string(),
|
|
50834
51756
|
description: zod_mini.z.string(),
|
|
50835
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
50836
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
50837
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
51757
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51758
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51759
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50838
51760
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
50839
51761
|
});
|
|
50840
51762
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
@@ -52748,6 +53670,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
52748
53670
|
supportsGlobal: true
|
|
52749
53671
|
}
|
|
52750
53672
|
}],
|
|
53673
|
+
["crush", {
|
|
53674
|
+
class: CrushSkill,
|
|
53675
|
+
meta: {
|
|
53676
|
+
supportsProject: true,
|
|
53677
|
+
supportsSimulated: false,
|
|
53678
|
+
supportsGlobal: true
|
|
53679
|
+
}
|
|
53680
|
+
}],
|
|
52751
53681
|
["cursor", {
|
|
52752
53682
|
class: CursorSkill,
|
|
52753
53683
|
meta: {
|
|
@@ -60491,6 +61421,238 @@ var ClineRule = class ClineRule extends ToolRule {
|
|
|
60491
61421
|
}
|
|
60492
61422
|
};
|
|
60493
61423
|
//#endregion
|
|
61424
|
+
//#region src/features/rules/codebuddy-rule.ts
|
|
61425
|
+
/**
|
|
61426
|
+
* Frontmatter schema for CodeBuddy Code modular rules.
|
|
61427
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61428
|
+
*/
|
|
61429
|
+
const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
|
|
61430
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
61431
|
+
paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
61432
|
+
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean())
|
|
61433
|
+
});
|
|
61434
|
+
/**
|
|
61435
|
+
* A universal glob (matching everything) is redundant on an Always Apply
|
|
61436
|
+
* rule and, paired with `alwaysApply: true`, is the same semantic conflict
|
|
61437
|
+
* `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
|
|
61438
|
+
* applies the rule everywhere, so also emitting an explicit
|
|
61439
|
+
* `paths: ["**\/*"]` is at best redundant and, on a subsequent
|
|
61440
|
+
* import/generate round-trip, misleadingly implies the rule is scoped by
|
|
61441
|
+
* path rather than always-on.
|
|
61442
|
+
*/
|
|
61443
|
+
const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
61444
|
+
/**
|
|
61445
|
+
* Rule generator for CodeBuddy Code, Tencent Cloud's terminal coding agent
|
|
61446
|
+
* (`@tencent-ai/codebuddy-code`). Its configuration surface mirrors Claude
|
|
61447
|
+
* Code closely.
|
|
61448
|
+
*
|
|
61449
|
+
* Rules format:
|
|
61450
|
+
* - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
|
|
61451
|
+
* - {project}/.codebuddy/rules/*.md (root: false, with optional
|
|
61452
|
+
* `description` / `paths` / `alwaysApply` frontmatter)
|
|
61453
|
+
* - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
|
|
61454
|
+
*
|
|
61455
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61456
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
61457
|
+
*/
|
|
61458
|
+
var CodebuddyRule = class CodebuddyRule extends ToolRule {
|
|
61459
|
+
frontmatter;
|
|
61460
|
+
body;
|
|
61461
|
+
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61462
|
+
if (global) return {
|
|
61463
|
+
root: {
|
|
61464
|
+
relativeDirPath: buildToolPath(CODEBUDDY_DIR, ".", excludeToolDir),
|
|
61465
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61466
|
+
},
|
|
61467
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61468
|
+
};
|
|
61469
|
+
return {
|
|
61470
|
+
root: {
|
|
61471
|
+
relativeDirPath: ".",
|
|
61472
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61473
|
+
},
|
|
61474
|
+
alternativeRoots: [{
|
|
61475
|
+
relativeDirPath: CODEBUDDY_DIR,
|
|
61476
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61477
|
+
}],
|
|
61478
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61479
|
+
};
|
|
61480
|
+
}
|
|
61481
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
61482
|
+
if (rest.validate) {
|
|
61483
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61484
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
61485
|
+
}
|
|
61486
|
+
super({
|
|
61487
|
+
...rest,
|
|
61488
|
+
fileContent: rest.root ? body : CodebuddyRule.generateFileContent(body, frontmatter)
|
|
61489
|
+
});
|
|
61490
|
+
this.frontmatter = frontmatter;
|
|
61491
|
+
this.body = body;
|
|
61492
|
+
}
|
|
61493
|
+
static generateFileContent(body, frontmatter) {
|
|
61494
|
+
if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
|
|
61495
|
+
return stringifyFrontmatter(body, {
|
|
61496
|
+
description: frontmatter.description,
|
|
61497
|
+
alwaysApply: frontmatter.alwaysApply,
|
|
61498
|
+
paths: frontmatter.paths
|
|
61499
|
+
});
|
|
61500
|
+
}
|
|
61501
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, relativeDirPath: overrideDirPath }) {
|
|
61502
|
+
const paths = this.getSettablePaths({ global });
|
|
61503
|
+
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61504
|
+
const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
|
|
61505
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, rootDirPath, paths.root.relativeFilePath));
|
|
61506
|
+
return new CodebuddyRule({
|
|
61507
|
+
outputRoot,
|
|
61508
|
+
relativeDirPath: rootDirPath,
|
|
61509
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61510
|
+
frontmatter: {},
|
|
61511
|
+
body: fileContent.trim(),
|
|
61512
|
+
validate,
|
|
61513
|
+
root: true
|
|
61514
|
+
});
|
|
61515
|
+
}
|
|
61516
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
|
|
61517
|
+
const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
61518
|
+
const filePath = (0, node_path.join)(outputRoot, relativePath);
|
|
61519
|
+
const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
61520
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61521
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
61522
|
+
return new CodebuddyRule({
|
|
61523
|
+
outputRoot,
|
|
61524
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61525
|
+
relativeFilePath,
|
|
61526
|
+
frontmatter: result.data,
|
|
61527
|
+
body: content.trim(),
|
|
61528
|
+
validate,
|
|
61529
|
+
root: false
|
|
61530
|
+
});
|
|
61531
|
+
}
|
|
61532
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61533
|
+
const isRoot = relativeFilePath === this.getSettablePaths({ global }).root.relativeFilePath;
|
|
61534
|
+
return new CodebuddyRule({
|
|
61535
|
+
outputRoot,
|
|
61536
|
+
relativeDirPath,
|
|
61537
|
+
relativeFilePath,
|
|
61538
|
+
frontmatter: {},
|
|
61539
|
+
body: "",
|
|
61540
|
+
validate: false,
|
|
61541
|
+
root: isRoot
|
|
61542
|
+
});
|
|
61543
|
+
}
|
|
61544
|
+
static resolveCodebuddyPaths({ paths, alwaysApply }) {
|
|
61545
|
+
if (!paths || paths.length === 0) return;
|
|
61546
|
+
if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
|
|
61547
|
+
return paths;
|
|
61548
|
+
}
|
|
61549
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
61550
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
61551
|
+
const root = rulesyncFrontmatter.root ?? false;
|
|
61552
|
+
const paths = this.getSettablePaths({ global });
|
|
61553
|
+
const body = rulesyncRule.getBody();
|
|
61554
|
+
if (root) return new CodebuddyRule({
|
|
61555
|
+
outputRoot,
|
|
61556
|
+
frontmatter: {},
|
|
61557
|
+
body,
|
|
61558
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
61559
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61560
|
+
validate,
|
|
61561
|
+
root
|
|
61562
|
+
});
|
|
61563
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
|
|
61564
|
+
const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
|
|
61565
|
+
const globs = rulesyncFrontmatter.globs;
|
|
61566
|
+
const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
|
|
61567
|
+
const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
|
|
61568
|
+
paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
|
|
61569
|
+
alwaysApply: alwaysApply === true
|
|
61570
|
+
});
|
|
61571
|
+
const codebuddyFrontmatter = {
|
|
61572
|
+
description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
|
|
61573
|
+
paths: pathsValue,
|
|
61574
|
+
alwaysApply
|
|
61575
|
+
};
|
|
61576
|
+
return new CodebuddyRule({
|
|
61577
|
+
outputRoot,
|
|
61578
|
+
frontmatter: codebuddyFrontmatter,
|
|
61579
|
+
body,
|
|
61580
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61581
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
61582
|
+
validate,
|
|
61583
|
+
root
|
|
61584
|
+
});
|
|
61585
|
+
}
|
|
61586
|
+
toRulesyncRule() {
|
|
61587
|
+
const targets = ["*"];
|
|
61588
|
+
if (this.isRoot()) {
|
|
61589
|
+
const rulesyncFrontmatter = {
|
|
61590
|
+
targets,
|
|
61591
|
+
root: true,
|
|
61592
|
+
description: this.description,
|
|
61593
|
+
globs: ["**/*"]
|
|
61594
|
+
};
|
|
61595
|
+
return new RulesyncRule({
|
|
61596
|
+
outputRoot: this.getOutputRoot(),
|
|
61597
|
+
frontmatter: rulesyncFrontmatter,
|
|
61598
|
+
body: this.body,
|
|
61599
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61600
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61601
|
+
validate: true
|
|
61602
|
+
});
|
|
61603
|
+
}
|
|
61604
|
+
const isAlways = this.frontmatter.alwaysApply === true;
|
|
61605
|
+
const sourcePaths = this.frontmatter.paths ?? [];
|
|
61606
|
+
const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
|
|
61607
|
+
const rulesyncFrontmatter = {
|
|
61608
|
+
targets,
|
|
61609
|
+
root: false,
|
|
61610
|
+
description: this.frontmatter.description,
|
|
61611
|
+
globs,
|
|
61612
|
+
...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
|
|
61613
|
+
paths: this.frontmatter.paths,
|
|
61614
|
+
alwaysApply: this.frontmatter.alwaysApply,
|
|
61615
|
+
description: this.frontmatter.description
|
|
61616
|
+
} }
|
|
61617
|
+
};
|
|
61618
|
+
return new RulesyncRule({
|
|
61619
|
+
outputRoot: this.getOutputRoot(),
|
|
61620
|
+
frontmatter: rulesyncFrontmatter,
|
|
61621
|
+
body: this.body,
|
|
61622
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61623
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61624
|
+
validate: true
|
|
61625
|
+
});
|
|
61626
|
+
}
|
|
61627
|
+
validate() {
|
|
61628
|
+
if (!this.frontmatter) return {
|
|
61629
|
+
success: true,
|
|
61630
|
+
error: null
|
|
61631
|
+
};
|
|
61632
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
61633
|
+
if (result.success) return {
|
|
61634
|
+
success: true,
|
|
61635
|
+
error: null
|
|
61636
|
+
};
|
|
61637
|
+
else return {
|
|
61638
|
+
success: false,
|
|
61639
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
61640
|
+
};
|
|
61641
|
+
}
|
|
61642
|
+
getFrontmatter() {
|
|
61643
|
+
return this.frontmatter;
|
|
61644
|
+
}
|
|
61645
|
+
getBody() {
|
|
61646
|
+
return this.body;
|
|
61647
|
+
}
|
|
61648
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
61649
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
61650
|
+
rulesyncRule,
|
|
61651
|
+
toolTarget: "codebuddy"
|
|
61652
|
+
});
|
|
61653
|
+
}
|
|
61654
|
+
};
|
|
61655
|
+
//#endregion
|
|
60494
61656
|
//#region src/features/rules/codexcli-rule.ts
|
|
60495
61657
|
var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
60496
61658
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -60787,6 +61949,79 @@ var CopilotcliRule = class CopilotcliRule extends CopilotRule {
|
|
|
60787
61949
|
}
|
|
60788
61950
|
};
|
|
60789
61951
|
//#endregion
|
|
61952
|
+
//#region src/features/rules/crush-rule.ts
|
|
61953
|
+
var CrushRule = class CrushRule extends ToolRule {
|
|
61954
|
+
constructor({ fileContent, root, ...rest }) {
|
|
61955
|
+
super({
|
|
61956
|
+
...rest,
|
|
61957
|
+
fileContent,
|
|
61958
|
+
root: root ?? false
|
|
61959
|
+
});
|
|
61960
|
+
}
|
|
61961
|
+
static getSettablePaths({ global = false } = {}) {
|
|
61962
|
+
if (global) return { root: {
|
|
61963
|
+
relativeDirPath: CRUSH_GLOBAL_DIR,
|
|
61964
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
61965
|
+
} };
|
|
61966
|
+
return { root: {
|
|
61967
|
+
relativeDirPath: ".",
|
|
61968
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
61969
|
+
} };
|
|
61970
|
+
}
|
|
61971
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
61972
|
+
const { root } = this.getSettablePaths({ global });
|
|
61973
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
61974
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
61975
|
+
return new CrushRule({
|
|
61976
|
+
outputRoot,
|
|
61977
|
+
relativeDirPath: root.relativeDirPath,
|
|
61978
|
+
relativeFilePath: root.relativeFilePath,
|
|
61979
|
+
fileContent,
|
|
61980
|
+
validate,
|
|
61981
|
+
root: true
|
|
61982
|
+
});
|
|
61983
|
+
}
|
|
61984
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
61985
|
+
const { root } = this.getSettablePaths({ global });
|
|
61986
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
61987
|
+
return new CrushRule({
|
|
61988
|
+
outputRoot,
|
|
61989
|
+
relativeDirPath: root.relativeDirPath,
|
|
61990
|
+
relativeFilePath: root.relativeFilePath,
|
|
61991
|
+
fileContent: rulesyncRule.getBody(),
|
|
61992
|
+
validate,
|
|
61993
|
+
root: isRoot
|
|
61994
|
+
});
|
|
61995
|
+
}
|
|
61996
|
+
toRulesyncRule() {
|
|
61997
|
+
return this.toRulesyncRuleDefault();
|
|
61998
|
+
}
|
|
61999
|
+
validate() {
|
|
62000
|
+
return {
|
|
62001
|
+
success: true,
|
|
62002
|
+
error: null
|
|
62003
|
+
};
|
|
62004
|
+
}
|
|
62005
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
62006
|
+
const { root } = this.getSettablePaths({ global });
|
|
62007
|
+
const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
|
|
62008
|
+
return new CrushRule({
|
|
62009
|
+
outputRoot,
|
|
62010
|
+
relativeDirPath,
|
|
62011
|
+
relativeFilePath,
|
|
62012
|
+
fileContent: "",
|
|
62013
|
+
validate: false,
|
|
62014
|
+
root: isRoot
|
|
62015
|
+
});
|
|
62016
|
+
}
|
|
62017
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
62018
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
62019
|
+
rulesyncRule,
|
|
62020
|
+
toolTarget: "crush"
|
|
62021
|
+
});
|
|
62022
|
+
}
|
|
62023
|
+
};
|
|
62024
|
+
//#endregion
|
|
60790
62025
|
//#region src/features/rules/cursor-rule.ts
|
|
60791
62026
|
const CursorRuleFrontmatterSchema = zod_mini.z.object({
|
|
60792
62027
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -61394,13 +62629,34 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
61394
62629
|
};
|
|
61395
62630
|
//#endregion
|
|
61396
62631
|
//#region src/features/rules/factorydroid-rule.ts
|
|
62632
|
+
/**
|
|
62633
|
+
* Rule generator for Factory Droid.
|
|
62634
|
+
*
|
|
62635
|
+
* Factory Droid loads the root `AGENTS.md` (project) / `~/.factory/AGENTS.md`
|
|
62636
|
+
* (global) as coding guidelines, plus non-root rules referenced from it via
|
|
62637
|
+
* `.factory/rules/*.md`.
|
|
62638
|
+
*
|
|
62639
|
+
* Factory Droid also loads `DESIGN.md` (project only) as a second,
|
|
62640
|
+
* independent instruction surface: "Always-on design-system, UX, visual, and
|
|
62641
|
+
* interaction guidance", loaded separately from `AGENTS.md`'s coding
|
|
62642
|
+
* guidelines. Rulesync emits it from any non-root rule that opts in via a
|
|
62643
|
+
* `factorydroid.channel: design` frontmatter block — those rule bodies are
|
|
62644
|
+
* routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
|
|
62645
|
+
* multiple opted-in rules concatenate in source order. Factory's docs describe
|
|
62646
|
+
* `DESIGN.md` at the repository root and in nested subdirectories, like
|
|
62647
|
+
* `AGENTS.md`, but document no personal/global home-directory equivalent, so
|
|
62648
|
+
* this channel is project scope only.
|
|
62649
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
62650
|
+
*/
|
|
61397
62651
|
var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
61398
|
-
|
|
62652
|
+
design;
|
|
62653
|
+
constructor({ fileContent, root, design = false, ...rest }) {
|
|
61399
62654
|
super({
|
|
61400
62655
|
...rest,
|
|
61401
62656
|
fileContent,
|
|
61402
62657
|
root: root ?? false
|
|
61403
62658
|
});
|
|
62659
|
+
this.design = design;
|
|
61404
62660
|
}
|
|
61405
62661
|
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61406
62662
|
if (global) return { root: {
|
|
@@ -61412,11 +62668,47 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61412
62668
|
relativeDirPath: ".",
|
|
61413
62669
|
relativeFilePath: FACTORYDROID_RULE_FILE_NAME
|
|
61414
62670
|
},
|
|
61415
|
-
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) }
|
|
62671
|
+
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) },
|
|
62672
|
+
design: {
|
|
62673
|
+
relativeDirPath: ".",
|
|
62674
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
|
|
62675
|
+
}
|
|
61416
62676
|
};
|
|
61417
62677
|
}
|
|
61418
|
-
|
|
62678
|
+
/**
|
|
62679
|
+
* Extra fixed files this tool manages beyond the root/non-root rules. The
|
|
62680
|
+
* RulesProcessor enumerates these for import and deletion so a stale
|
|
62681
|
+
* `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
|
|
62682
|
+
* mode: `DESIGN.md` has no documented home-directory equivalent.
|
|
62683
|
+
*/
|
|
62684
|
+
static getExtraFixedFiles({ global = false } = {}) {
|
|
62685
|
+
if (global) return [];
|
|
62686
|
+
return [this.getSettablePaths({ global }).design];
|
|
62687
|
+
}
|
|
62688
|
+
/**
|
|
62689
|
+
* Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
|
|
62690
|
+
* TOON reference section would double-load the content (and misrepresent it
|
|
62691
|
+
* as a rule the model must remember to open).
|
|
62692
|
+
*/
|
|
62693
|
+
isExcludedFromRootReferences() {
|
|
62694
|
+
return this.design;
|
|
62695
|
+
}
|
|
62696
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
61419
62697
|
const paths = this.getSettablePaths({ global });
|
|
62698
|
+
const design = !global ? paths.design : void 0;
|
|
62699
|
+
if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
|
|
62700
|
+
const relativePath = (0, node_path.join)(design.relativeDirPath, design.relativeFilePath);
|
|
62701
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
62702
|
+
return new FactorydroidRule({
|
|
62703
|
+
outputRoot,
|
|
62704
|
+
relativeDirPath: design.relativeDirPath,
|
|
62705
|
+
relativeFilePath: design.relativeFilePath,
|
|
62706
|
+
fileContent,
|
|
62707
|
+
validate,
|
|
62708
|
+
root: false,
|
|
62709
|
+
design: true
|
|
62710
|
+
});
|
|
62711
|
+
}
|
|
61420
62712
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61421
62713
|
const relativePath = (0, node_path.join)(paths.root.relativeDirPath, paths.root.relativeFilePath);
|
|
61422
62714
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
@@ -61443,18 +62735,34 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61443
62735
|
}
|
|
61444
62736
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61445
62737
|
const paths = this.getSettablePaths({ global });
|
|
61446
|
-
const
|
|
62738
|
+
const design = !global ? paths.design : void 0;
|
|
62739
|
+
const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
|
|
62740
|
+
const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
61447
62741
|
return new FactorydroidRule({
|
|
61448
62742
|
outputRoot,
|
|
61449
62743
|
relativeDirPath,
|
|
61450
62744
|
relativeFilePath,
|
|
61451
62745
|
fileContent: "",
|
|
61452
62746
|
validate: false,
|
|
61453
|
-
root: isRoot
|
|
62747
|
+
root: isRoot,
|
|
62748
|
+
design: isDesign
|
|
61454
62749
|
});
|
|
61455
62750
|
}
|
|
61456
62751
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
62752
|
+
const frontmatter = rulesyncRule.getFrontmatter();
|
|
61457
62753
|
const paths = this.getSettablePaths({ global });
|
|
62754
|
+
if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
|
|
62755
|
+
const { design } = paths;
|
|
62756
|
+
return new FactorydroidRule({
|
|
62757
|
+
outputRoot,
|
|
62758
|
+
relativeDirPath: design.relativeDirPath,
|
|
62759
|
+
relativeFilePath: design.relativeFilePath,
|
|
62760
|
+
fileContent: rulesyncRule.getBody(),
|
|
62761
|
+
validate,
|
|
62762
|
+
root: false,
|
|
62763
|
+
design: true
|
|
62764
|
+
});
|
|
62765
|
+
}
|
|
61458
62766
|
return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
|
|
61459
62767
|
outputRoot,
|
|
61460
62768
|
rulesyncRule,
|
|
@@ -61464,6 +62772,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61464
62772
|
}));
|
|
61465
62773
|
}
|
|
61466
62774
|
toRulesyncRule() {
|
|
62775
|
+
if (this.design) return new RulesyncRule({
|
|
62776
|
+
outputRoot: process.cwd(),
|
|
62777
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
62778
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
|
|
62779
|
+
frontmatter: {
|
|
62780
|
+
root: false,
|
|
62781
|
+
targets: ["factorydroid"],
|
|
62782
|
+
factorydroid: { channel: "design" }
|
|
62783
|
+
},
|
|
62784
|
+
body: this.getFileContent()
|
|
62785
|
+
});
|
|
61467
62786
|
return this.toRulesyncRuleDefault();
|
|
61468
62787
|
}
|
|
61469
62788
|
validate() {
|
|
@@ -63981,6 +65300,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
63981
65300
|
ruleDiscoveryMode: "auto"
|
|
63982
65301
|
}
|
|
63983
65302
|
}],
|
|
65303
|
+
["codebuddy", {
|
|
65304
|
+
class: CodebuddyRule,
|
|
65305
|
+
meta: {
|
|
65306
|
+
extension: "md",
|
|
65307
|
+
supportsGlobal: true,
|
|
65308
|
+
ruleDiscoveryMode: "auto",
|
|
65309
|
+
localRootMode: "separate-local-file",
|
|
65310
|
+
localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
|
|
65311
|
+
}
|
|
65312
|
+
}],
|
|
63984
65313
|
["codexcli", {
|
|
63985
65314
|
class: CodexcliRule,
|
|
63986
65315
|
meta: {
|
|
@@ -64006,6 +65335,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
64006
65335
|
ruleDiscoveryMode: "auto"
|
|
64007
65336
|
}
|
|
64008
65337
|
}],
|
|
65338
|
+
["crush", {
|
|
65339
|
+
class: CrushRule,
|
|
65340
|
+
meta: {
|
|
65341
|
+
extension: "md",
|
|
65342
|
+
supportsGlobal: true,
|
|
65343
|
+
ruleDiscoveryMode: "auto",
|
|
65344
|
+
collisionPolicy: "fold"
|
|
65345
|
+
}
|
|
65346
|
+
}],
|
|
64009
65347
|
["cursor", {
|
|
64010
65348
|
class: CursorRule,
|
|
64011
65349
|
meta: {
|
|
@@ -64374,6 +65712,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64374
65712
|
outputFiles,
|
|
64375
65713
|
convertedRules
|
|
64376
65714
|
});
|
|
65715
|
+
await this.warnForDeactivatedImportOnlyRoots({
|
|
65716
|
+
toolRules,
|
|
65717
|
+
factory
|
|
65718
|
+
});
|
|
64377
65719
|
return outputFiles;
|
|
64378
65720
|
}
|
|
64379
65721
|
/**
|
|
@@ -64595,6 +65937,41 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64595
65937
|
}
|
|
64596
65938
|
}
|
|
64597
65939
|
/**
|
|
65940
|
+
* Warn when this generate run is about to write a root rule file that will
|
|
65941
|
+
* make the tool stop reading paths it currently reads instead — Junie's
|
|
65942
|
+
* `.junie/rules/*.md` and `.junie/playbook.md` become unreachable the
|
|
65943
|
+
* moment `.junie/AGENTS.md` exists, since Junie reads the root file
|
|
65944
|
+
* exclusively once it is present. `importOnlyRoots` with
|
|
65945
|
+
* `onlyWhenRootAbsent` already models exactly this shape for import; this
|
|
65946
|
+
* reuses the same declaration so the
|
|
65947
|
+
* `generate` path — which never calls `loadToolFiles` and so never reached
|
|
65948
|
+
* the existing import-side warning — surfaces it too. Without this, a repo
|
|
65949
|
+
* that only ever runs `generate` never sees any warning: the deactivated
|
|
65950
|
+
* files stay on disk, untouched and not gitignored, silently unread.
|
|
65951
|
+
*/
|
|
65952
|
+
async warnForDeactivatedImportOnlyRoots({ toolRules, factory }) {
|
|
65953
|
+
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
65954
|
+
if (!rootRule) return;
|
|
65955
|
+
const settablePaths = factory.class.getSettablePaths({ global: this.global });
|
|
65956
|
+
const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
|
|
65957
|
+
if (!importOnlyRoots || importOnlyRoots.length === 0) return;
|
|
65958
|
+
const existingPaths = [];
|
|
65959
|
+
for (const importOnlyRoot of importOnlyRoots) {
|
|
65960
|
+
if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
|
|
65961
|
+
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
65962
|
+
cwd: this.outputRoot,
|
|
65963
|
+
type: "file"
|
|
65964
|
+
});
|
|
65965
|
+
existingPaths.push(...matchedPaths);
|
|
65966
|
+
}
|
|
65967
|
+
if (existingPaths.length === 0) return;
|
|
65968
|
+
const rootFileRelativePath = (0, node_path.join)(rootRule.getRelativeDirPath(), rootRule.getRelativeFilePath());
|
|
65969
|
+
const names = existingPaths.map((filePath) => stripControlCharacters((0, node_path.relative)(this.outputRoot, filePath)));
|
|
65970
|
+
const listedNames = names.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
65971
|
+
const remainingCount = names.length - listedNames.length;
|
|
65972
|
+
this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
|
|
65973
|
+
}
|
|
65974
|
+
/**
|
|
64598
65975
|
* Handle localRoot rule generation based on tool target.
|
|
64599
65976
|
* - `separate-local-file`: writes a dedicated `*.local.md` root file
|
|
64600
65977
|
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
|
|
@@ -64846,6 +66223,26 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64846
66223
|
}));
|
|
64847
66224
|
}
|
|
64848
66225
|
/**
|
|
66226
|
+
* Load and merge rulesync rule files from every configured input root's
|
|
66227
|
+
* `.rulesync/rules/` directory, by relative path, so that a rule with the
|
|
66228
|
+
* same target path from a later root replaces the earlier root's copy
|
|
66229
|
+
* (case-insensitive, matching the intra-root collision handling).
|
|
66230
|
+
*
|
|
66231
|
+
* This is the side-effect-free half of `loadRulesyncFiles`: it does not
|
|
66232
|
+
* warn about a missing root rule or validate `localRoot` placement, so it
|
|
66233
|
+
* is also safe to call from code paths — like
|
|
66234
|
+
* `warnForFoldImportDuplicationRisk` — that only need the merged rule set
|
|
66235
|
+
* and must not trigger `loadRulesyncFiles`'s generate-time checks.
|
|
66236
|
+
*/
|
|
66237
|
+
async loadMergedRulesyncRules() {
|
|
66238
|
+
return mergeByCaseInsensitiveIdentity({
|
|
66239
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
66240
|
+
identity: (rule) => rule.getRelativeFilePath(),
|
|
66241
|
+
artifactName: "rule",
|
|
66242
|
+
logger: this.logger
|
|
66243
|
+
});
|
|
66244
|
+
}
|
|
66245
|
+
/**
|
|
64849
66246
|
* Implementation of abstract method from FeatureProcessor
|
|
64850
66247
|
* Load and parse rulesync rule files from every configured input root's
|
|
64851
66248
|
* `.rulesync/rules/` directory, merging by relative path so that a rule
|
|
@@ -64853,12 +66250,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64853
66250
|
* copy (case-insensitive, matching the intra-root collision handling).
|
|
64854
66251
|
*/
|
|
64855
66252
|
async loadRulesyncFiles() {
|
|
64856
|
-
const rulesyncRules =
|
|
64857
|
-
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
64858
|
-
identity: (rule) => rule.getRelativeFilePath(),
|
|
64859
|
-
artifactName: "rule",
|
|
64860
|
-
logger: this.logger
|
|
64861
|
-
});
|
|
66253
|
+
const rulesyncRules = await this.loadMergedRulesyncRules();
|
|
64862
66254
|
const factory = this.getFactory(this.toolTarget);
|
|
64863
66255
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
64864
66256
|
if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
|
|
@@ -64915,6 +66307,46 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64915
66307
|
});
|
|
64916
66308
|
}
|
|
64917
66309
|
/**
|
|
66310
|
+
* Warn when importing a `collisionPolicy: "fold"` target's root file while
|
|
66311
|
+
* `.rulesync/rules/` still holds non-root rules targeting it. A fold target
|
|
66312
|
+
* (codexcli, junie, and others) concatenates every targeted non-root rule
|
|
66313
|
+
* into its one root output file on `generate`. Importing that root file
|
|
66314
|
+
* back therefore re-reads the already-folded content as a single new
|
|
66315
|
+
* rulesync rule, while the original non-root rules stay in place
|
|
66316
|
+
* untouched — the next `generate` folds both together, duplicating the
|
|
66317
|
+
* content once per generate/import cycle with nothing to indicate why.
|
|
66318
|
+
*
|
|
66319
|
+
* This does not attempt to detect or drop the specific duplicated content
|
|
66320
|
+
* (the root file has no marker recording which rule contributed what); it
|
|
66321
|
+
* only surfaces that the cycle produces one, per the "at minimum, warn"
|
|
66322
|
+
* option recorded on issue #2743.
|
|
66323
|
+
*
|
|
66324
|
+
* Only the actual `rulesync import` call site invokes this (and only once
|
|
66325
|
+
* it has confirmed there is something to import) — `loadToolFiles` is also
|
|
66326
|
+
* the entry point for `rulesync convert` and `rulesync fetch`, neither of
|
|
66327
|
+
* which writes to `.rulesync/rules/` or carries this duplication risk.
|
|
66328
|
+
*
|
|
66329
|
+
* Reads via `loadMergedRulesyncRules` rather than `loadRulesyncFiles`
|
|
66330
|
+
* deliberately: this runs before the imported root file is written, so
|
|
66331
|
+
* `.rulesync/rules/` never yet has a root rule targeting this tool, and
|
|
66332
|
+
* `loadRulesyncFiles`'s "no root rule found" warning and `localRoot`
|
|
66333
|
+
* validation (which can throw) would fire spuriously on every fold-tool
|
|
66334
|
+
* import — including ones where nothing is actually misconfigured.
|
|
66335
|
+
*
|
|
66336
|
+
* In global mode, a `localRoot: true` rule is excluded from the
|
|
66337
|
+
* duplication check the same way `loadRulesyncFiles`'s global-mode branch
|
|
66338
|
+
* excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
|
|
66339
|
+
* in global mode, so such a rule is never actually folded into the global
|
|
66340
|
+
* root output and warning about it here would be inaccurate.
|
|
66341
|
+
*/
|
|
66342
|
+
async warnForFoldImportDuplicationRisk() {
|
|
66343
|
+
const factory = this.getFactory(this.toolTarget);
|
|
66344
|
+
if (factory.meta.collisionPolicy !== "fold") return;
|
|
66345
|
+
const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
|
|
66346
|
+
if (nonRootRules.length === 0) return;
|
|
66347
|
+
this.logger.warn(`Importing ${this.toolTarget}'s root file will re-add content already folded from ${formatRulePaths(nonRootRules)}: ${this.toolTarget} concatenates every non-root rule into its single root output file, so the imported copy duplicates them the next time you run \`rulesync generate --targets ${this.toolTarget}\`. Review the imported rule and remove the duplicated content, or remove the original non-root rule files, before generating again.`);
|
|
66348
|
+
}
|
|
66349
|
+
/**
|
|
64918
66350
|
* Implementation of abstract method from FeatureProcessor
|
|
64919
66351
|
* Load tool-specific rule configurations and parse them into ToolRule instances
|
|
64920
66352
|
*/
|
|
@@ -66978,6 +68410,7 @@ async function importRulesCore(params) {
|
|
|
66978
68410
|
logger.warn(`No rule files found for ${tool}. Skipping import.`);
|
|
66979
68411
|
return 0;
|
|
66980
68412
|
}
|
|
68413
|
+
await rulesProcessor.warnForFoldImportDuplicationRisk();
|
|
66981
68414
|
const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
66982
68415
|
const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
|
|
66983
68416
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);
|
|
@@ -67297,6 +68730,18 @@ Object.defineProperty(exports, "CLIError", {
|
|
|
67297
68730
|
return CLIError;
|
|
67298
68731
|
}
|
|
67299
68732
|
});
|
|
68733
|
+
Object.defineProperty(exports, "CODEBUDDY_DIR", {
|
|
68734
|
+
enumerable: true,
|
|
68735
|
+
get: function() {
|
|
68736
|
+
return CODEBUDDY_DIR;
|
|
68737
|
+
}
|
|
68738
|
+
});
|
|
68739
|
+
Object.defineProperty(exports, "CODEBUDDY_LOCAL_RULE_FILE_NAME", {
|
|
68740
|
+
enumerable: true,
|
|
68741
|
+
get: function() {
|
|
68742
|
+
return CODEBUDDY_LOCAL_RULE_FILE_NAME;
|
|
68743
|
+
}
|
|
68744
|
+
});
|
|
67300
68745
|
Object.defineProperty(exports, "CODEXCLI_BASH_RULES_FILE_NAME", {
|
|
67301
68746
|
enumerable: true,
|
|
67302
68747
|
get: function() {
|
|
@@ -67879,6 +69324,12 @@ Object.defineProperty(exports, "hasDeceptiveHiddenCharacters", {
|
|
|
67879
69324
|
return hasDeceptiveHiddenCharacters;
|
|
67880
69325
|
}
|
|
67881
69326
|
});
|
|
69327
|
+
Object.defineProperty(exports, "hasEnclosingMarkOutsideKeycap", {
|
|
69328
|
+
enumerable: true,
|
|
69329
|
+
get: function() {
|
|
69330
|
+
return hasEnclosingMarkOutsideKeycap;
|
|
69331
|
+
}
|
|
69332
|
+
});
|
|
67882
69333
|
Object.defineProperty(exports, "importFromTool", {
|
|
67883
69334
|
enumerable: true,
|
|
67884
69335
|
get: function() {
|