rulesync 16.21.0 → 16.22.1
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-7UmDF35I.cjs → import-B7o8So93.cjs} +2203 -683
- package/dist/{import-5_n-Y8N6.js → import-DUE1P1zV.js} +2170 -668
- package/dist/import-DUE1P1zV.js.map +1 -0
- 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
|
@@ -221,6 +221,35 @@ function hasDeceptiveHiddenCharacters(text) {
|
|
|
221
221
|
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
|
+
/** A mark that draws around the character before it — a circle, a square, a keycap box. */
|
|
225
|
+
const ENCLOSING_MARK_PATTERN = /\p{Me}/u;
|
|
226
|
+
/**
|
|
227
|
+
* Whether `text` carries an enclosing mark that is not the keycap of an emoji
|
|
228
|
+
* keycap sequence.
|
|
229
|
+
*
|
|
230
|
+
* An enclosing mark (`\p{Me}`: U+20DD COMBINING ENCLOSING CIRCLE, U+20E3
|
|
231
|
+
* COMBINING ENCLOSING KEYCAP, the Cyrillic and Vedic ones) is drawn over the
|
|
232
|
+
* character before it and takes no column of its own, so `pdf` with one after
|
|
233
|
+
* it occupies the three columns of `pdf` and is a fourth directory underneath.
|
|
234
|
+
* Unlike a joiner or a variation selector it is not invisible — the box is
|
|
235
|
+
* drawn — which is why `hasDeceptiveHiddenCharacters` does not refuse it and
|
|
236
|
+
* why it is a question for the confusable-name note instead: the row is drawn,
|
|
237
|
+
* only not the way its name reads. The one place an enclosing mark belongs in
|
|
238
|
+
* a name is the keycap sequence of UTS #51, which `isKeycapSequence` matches
|
|
239
|
+
* whole; every other one is left over.
|
|
240
|
+
*
|
|
241
|
+
* Restricted to `\p{Me}` on purpose: a non-spacing mark (`\p{Mn}`) is how
|
|
242
|
+
* Devanagari, Arabic and Vietnamese write, and folding those would mark
|
|
243
|
+
* ordinary names in every one of them.
|
|
244
|
+
*/
|
|
245
|
+
function hasEnclosingMarkOutsideKeycap(text) {
|
|
246
|
+
const characters = [...text];
|
|
247
|
+
return characters.some((character, index) => ENCLOSING_MARK_PATTERN.test(character) && !isKeycapSequence({
|
|
248
|
+
base: characters[index - 2],
|
|
249
|
+
selector: characters[index - 1] ?? "",
|
|
250
|
+
following: character
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
224
253
|
//#endregion
|
|
225
254
|
//#region src/utils/truncate.ts
|
|
226
255
|
/**
|
|
@@ -444,9 +473,11 @@ const rulesProcessorToolTargetTuple = [
|
|
|
444
473
|
"claudecode",
|
|
445
474
|
"claudecode-legacy",
|
|
446
475
|
"cline",
|
|
476
|
+
"codebuddy",
|
|
447
477
|
"codexcli",
|
|
448
478
|
"copilot",
|
|
449
479
|
"copilotcli",
|
|
480
|
+
"crush",
|
|
450
481
|
"cursor",
|
|
451
482
|
"deepagents",
|
|
452
483
|
"factorydroid",
|
|
@@ -482,6 +513,7 @@ const ignoreProcessorToolTargetTuple = [
|
|
|
482
513
|
"claudecode",
|
|
483
514
|
"claudecode-legacy",
|
|
484
515
|
"cline",
|
|
516
|
+
"crush",
|
|
485
517
|
"cursor",
|
|
486
518
|
"hermesagent",
|
|
487
519
|
"junie",
|
|
@@ -623,6 +655,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
623
655
|
"codexcli",
|
|
624
656
|
"copilot",
|
|
625
657
|
"copilotcli",
|
|
658
|
+
"crush",
|
|
626
659
|
"cursor",
|
|
627
660
|
"deepagents",
|
|
628
661
|
"factorydroid",
|
|
@@ -3327,6 +3360,83 @@ var RulesyncFile = class extends AiFile {
|
|
|
3327
3360
|
}
|
|
3328
3361
|
};
|
|
3329
3362
|
//#endregion
|
|
3363
|
+
//#region src/utils/bounded-walk.ts
|
|
3364
|
+
const ALIAS_HINT = "(a chain of YAML aliases may be amplifying the document)";
|
|
3365
|
+
/**
|
|
3366
|
+
* Create the bookkeeping for one walk. `subject` names the document kind in
|
|
3367
|
+
* every error ("Frontmatter", "Shared config"); `root`, when given, is entered
|
|
3368
|
+
* up front so the root object counts as the first nesting level, matching the
|
|
3369
|
+
* +1 that `enter` applies to every container nested inside it.
|
|
3370
|
+
*/
|
|
3371
|
+
function createBoundedWalk({ subject, limits, root }) {
|
|
3372
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
3373
|
+
let valuesRemaining = limits.maxValues;
|
|
3374
|
+
let stringCharsRemaining = limits.maxStringChars;
|
|
3375
|
+
let depth = 0;
|
|
3376
|
+
const chargeChars = (chars) => {
|
|
3377
|
+
stringCharsRemaining -= chars;
|
|
3378
|
+
if (stringCharsRemaining < 0) throw new Error(`${subject}'s string values expand to more than ${limits.maxStringChars} characters; refusing to process it ${ALIAS_HINT}`);
|
|
3379
|
+
};
|
|
3380
|
+
const chargeValue = (stringChars = 0) => {
|
|
3381
|
+
valuesRemaining -= 1;
|
|
3382
|
+
if (valuesRemaining < 0) throw new Error(`${subject} expands to more than ${limits.maxValues} values; refusing to process it ${ALIAS_HINT}`);
|
|
3383
|
+
chargeChars(stringChars);
|
|
3384
|
+
};
|
|
3385
|
+
const enter = (container) => {
|
|
3386
|
+
depth += 1;
|
|
3387
|
+
if (depth > limits.maxDepth) throw new Error(`${subject} nests more than ${limits.maxDepth} levels deep; refusing to process it ${ALIAS_HINT}`);
|
|
3388
|
+
ancestors.add(container);
|
|
3389
|
+
};
|
|
3390
|
+
const leave = (container) => {
|
|
3391
|
+
ancestors.delete(container);
|
|
3392
|
+
depth -= 1;
|
|
3393
|
+
};
|
|
3394
|
+
if (root !== void 0) enter(root);
|
|
3395
|
+
return {
|
|
3396
|
+
chargeValue,
|
|
3397
|
+
chargeChars,
|
|
3398
|
+
isAncestor: (container) => ancestors.has(container),
|
|
3399
|
+
enter,
|
|
3400
|
+
leave
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
//#endregion
|
|
3404
|
+
//#region src/utils/prototype-pollution.ts
|
|
3405
|
+
/**
|
|
3406
|
+
* Keys that, if walked into when constructing or merging objects from
|
|
3407
|
+
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
3408
|
+
* chain) and propagate state to every other object in the runtime. Any code
|
|
3409
|
+
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
3410
|
+
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
3411
|
+
*/
|
|
3412
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
3413
|
+
"__proto__",
|
|
3414
|
+
"constructor",
|
|
3415
|
+
"prototype"
|
|
3416
|
+
]);
|
|
3417
|
+
function isPrototypePollutionKey(key) {
|
|
3418
|
+
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Returns a shallow copy of a record's own entries with every
|
|
3422
|
+
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
3423
|
+
*
|
|
3424
|
+
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
3425
|
+
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
3426
|
+
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
3427
|
+
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
3428
|
+
* prototype). Walking the entries through this helper severs that path while
|
|
3429
|
+
* preserving every legitimate key.
|
|
3430
|
+
*/
|
|
3431
|
+
function omitPrototypePollutionKeys(record) {
|
|
3432
|
+
const sanitized = {};
|
|
3433
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3434
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3435
|
+
sanitized[key] = value;
|
|
3436
|
+
}
|
|
3437
|
+
return sanitized;
|
|
3438
|
+
}
|
|
3439
|
+
//#endregion
|
|
3330
3440
|
//#region src/utils/type-guards.ts
|
|
3331
3441
|
/**
|
|
3332
3442
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
@@ -3391,67 +3501,154 @@ function loadYaml(content) {
|
|
|
3391
3501
|
}
|
|
3392
3502
|
//#endregion
|
|
3393
3503
|
//#region src/utils/frontmatter.ts
|
|
3394
|
-
|
|
3504
|
+
/**
|
|
3505
|
+
* Upper bound on the number of values a frontmatter document may expand to
|
|
3506
|
+
* once every YAML alias is written out.
|
|
3507
|
+
*
|
|
3508
|
+
* A YAML alias makes one parsed container reachable from many keys, and the
|
|
3509
|
+
* cleaners below copy each reachable value, so a small file with a few levels
|
|
3510
|
+
* of nested aliases (an "alias bomb") can expand into megabytes of output or
|
|
3511
|
+
* exhaust the heap. Counting every visited value against this budget turns
|
|
3512
|
+
* that into an error instead. Real frontmatter is a handful of keys; even a
|
|
3513
|
+
* generous skill manifest stays orders of magnitude below the limit.
|
|
3514
|
+
*/
|
|
3515
|
+
const MAX_FRONTMATTER_VALUES = 1e5;
|
|
3516
|
+
/**
|
|
3517
|
+
* Upper bound on the total character count of string leaves a frontmatter
|
|
3518
|
+
* document may expand to.
|
|
3519
|
+
*
|
|
3520
|
+
* {@link MAX_FRONTMATTER_VALUES} bounds how many values are visited, but a
|
|
3521
|
+
* single long string aliased thousands of times still fits that budget while
|
|
3522
|
+
* the duplicated output balloons: one scalar of a few KB, chained through a
|
|
3523
|
+
* handful of aliases within the value budget, can multiply into a document
|
|
3524
|
+
* many megabytes larger than it started. Charging every visited string's
|
|
3525
|
+
* length against this separate budget bounds that output regardless of how
|
|
3526
|
+
* many aliases point at it.
|
|
3527
|
+
*/
|
|
3528
|
+
const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
3529
|
+
/**
|
|
3530
|
+
* Upper bound on the raw character length of the `---`-delimited frontmatter
|
|
3531
|
+
* block itself, checked before it is ever handed to the YAML parser.
|
|
3532
|
+
*
|
|
3533
|
+
* The budgets above only bound the *parsed* document — the walk over
|
|
3534
|
+
* `matter()`'s output — but a complex YAML key (an array or mapping used as a
|
|
3535
|
+
* mapping key) is joined into a string by js-yaml while it parses, and a
|
|
3536
|
+
* mapping with many such keys can cost real memory before that walk ever
|
|
3537
|
+
* starts, or even before `matter()` returns. Capping the raw block size keeps
|
|
3538
|
+
* that parse-time cost bounded regardless of what the block contains. Real
|
|
3539
|
+
* frontmatter blocks are a few hundred bytes at most; even a large project
|
|
3540
|
+
* manifest stays well under this.
|
|
3541
|
+
*/
|
|
3542
|
+
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3543
|
+
/**
|
|
3544
|
+
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3545
|
+
* string leaf is charged by its own length instead).
|
|
3546
|
+
*
|
|
3547
|
+
* js-yaml's default schema resolves `!!binary` scalars to a `Uint8Array`, and
|
|
3548
|
+
* its dumper writes one back out as base64 — roughly 4 output characters per
|
|
3549
|
+
* 3 input bytes. Without this, an aliased binary blob would walk the budget
|
|
3550
|
+
* for free even though it can dominate the emitted document's size.
|
|
3551
|
+
*/
|
|
3552
|
+
function estimateLeafChars(value) {
|
|
3553
|
+
if (value instanceof Uint8Array) return Math.ceil(value.byteLength / 3) * 4;
|
|
3554
|
+
return 0;
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Copy one parsed value, dropping nullish leaves and cyclic references.
|
|
3558
|
+
*
|
|
3559
|
+
* Every alias is still written out as an independent copy, as gray-matter's
|
|
3560
|
+
* default YAML engine would otherwise serialize shared references as `&ref_0`
|
|
3561
|
+
* anchors that simplified frontmatter parsers cannot read; the expansion is
|
|
3562
|
+
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3563
|
+
*/
|
|
3564
|
+
function deepCleanValue(value, options) {
|
|
3565
|
+
const leafChars = typeof value === "string" ? value.length : estimateLeafChars(value);
|
|
3566
|
+
options.walk.chargeValue(leafChars);
|
|
3395
3567
|
if (value === null || value === void 0) return;
|
|
3396
|
-
if (
|
|
3568
|
+
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3569
|
+
if (Array.isArray(value)) {
|
|
3570
|
+
if (options.walk.isAncestor(value)) return;
|
|
3571
|
+
options.walk.enter(value);
|
|
3572
|
+
const cleanedArray = [];
|
|
3573
|
+
for (const item of value) {
|
|
3574
|
+
const cleaned = deepCleanValue(item, options);
|
|
3575
|
+
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3576
|
+
}
|
|
3577
|
+
options.walk.leave(value);
|
|
3578
|
+
return cleanedArray;
|
|
3579
|
+
}
|
|
3397
3580
|
if (isPlainObject$1(value)) {
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3581
|
+
if (options.walk.isAncestor(value)) return;
|
|
3582
|
+
options.walk.enter(value);
|
|
3583
|
+
const result = cleanOwnEntries(value, options);
|
|
3584
|
+
options.walk.leave(value);
|
|
3403
3585
|
return result;
|
|
3404
3586
|
}
|
|
3405
3587
|
return value;
|
|
3406
3588
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3589
|
+
/**
|
|
3590
|
+
* Copy the cleaned own entries of a parsed object into a fresh record.
|
|
3591
|
+
*
|
|
3592
|
+
* A YAML parser defines a `__proto__:` key as an own property, and assigning
|
|
3593
|
+
* it back with bracket notation would instead replace the new record's
|
|
3594
|
+
* prototype, whose members zod's loose object schemas then promote to real
|
|
3595
|
+
* keys. So a fetched skill could hide `allowed-tools` under an innocuous
|
|
3596
|
+
* looking `__proto__:` block. That key, `constructor` and `prototype` are
|
|
3597
|
+
* therefore dropped rather than copied, and cannot be used as frontmatter
|
|
3598
|
+
* keys.
|
|
3599
|
+
*/
|
|
3600
|
+
function cleanOwnEntries(obj, options) {
|
|
3409
3601
|
const result = {};
|
|
3410
3602
|
for (const [key, val] of Object.entries(obj)) {
|
|
3411
|
-
|
|
3603
|
+
options.walk.chargeChars(key.length);
|
|
3604
|
+
const cleaned = deepCleanValue(val, options);
|
|
3605
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
3412
3606
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
3413
3607
|
}
|
|
3414
3608
|
return result;
|
|
3415
3609
|
}
|
|
3416
|
-
function
|
|
3417
|
-
if (
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3610
|
+
function deepCleanObject(obj, options) {
|
|
3611
|
+
if (!obj || typeof obj !== "object") return {};
|
|
3612
|
+
return cleanOwnEntries(obj, {
|
|
3613
|
+
...options,
|
|
3614
|
+
walk: createBoundedWalk({
|
|
3615
|
+
subject: "Frontmatter",
|
|
3616
|
+
limits: {
|
|
3617
|
+
maxValues: MAX_FRONTMATTER_VALUES,
|
|
3618
|
+
maxStringChars: MAX_FRONTMATTER_STRING_CHARS,
|
|
3619
|
+
maxDepth: 64
|
|
3620
|
+
},
|
|
3621
|
+
root: obj
|
|
3622
|
+
})
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
/** Drop null and undefined values, recursively. */
|
|
3626
|
+
function deepRemoveNullishObject(obj) {
|
|
3627
|
+
return deepCleanObject(obj, {});
|
|
3429
3628
|
}
|
|
3629
|
+
/** Drop nullish values and collapse every string onto a single line. */
|
|
3430
3630
|
function deepFlattenStringsObject(obj) {
|
|
3431
|
-
|
|
3432
|
-
const result = {};
|
|
3433
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
3434
|
-
const cleaned = deepFlattenStringsValue(val);
|
|
3435
|
-
if (cleaned !== void 0) result[key] = cleaned;
|
|
3436
|
-
}
|
|
3437
|
-
return result;
|
|
3631
|
+
return deepCleanObject(obj, { transformString: (value) => value.replace(/\n+/g, " ").trim() });
|
|
3438
3632
|
}
|
|
3439
3633
|
function stringifyFrontmatter(body, frontmatter, options) {
|
|
3440
3634
|
const { avoidBlockScalars = false } = options ?? {};
|
|
3441
3635
|
const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
|
|
3442
|
-
|
|
3636
|
+
const file = { content: body };
|
|
3637
|
+
if (avoidBlockScalars) return matter.stringify(file, cleanFrontmatter, { engines: { yaml: {
|
|
3443
3638
|
parse: (input) => loadYaml(input) ?? {},
|
|
3444
3639
|
stringify: (data) => dump(data, { lineWidth: -1 })
|
|
3445
3640
|
} } });
|
|
3446
|
-
return matter.stringify(
|
|
3641
|
+
return matter.stringify(file, cleanFrontmatter);
|
|
3447
3642
|
}
|
|
3448
3643
|
function parseFrontmatter(content, filePath) {
|
|
3449
3644
|
let frontmatter;
|
|
3450
3645
|
let body;
|
|
3451
3646
|
let hasFrontmatter;
|
|
3452
3647
|
try {
|
|
3648
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3649
|
+
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)`);
|
|
3453
3650
|
const result = matter(content, {});
|
|
3454
|
-
frontmatter = result.data;
|
|
3651
|
+
frontmatter = deepRemoveNullishObject(result.data);
|
|
3455
3652
|
body = result.content;
|
|
3456
3653
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
3457
3654
|
} catch (error) {
|
|
@@ -3459,7 +3656,7 @@ function parseFrontmatter(content, filePath) {
|
|
|
3459
3656
|
throw error;
|
|
3460
3657
|
}
|
|
3461
3658
|
return {
|
|
3462
|
-
frontmatter
|
|
3659
|
+
frontmatter,
|
|
3463
3660
|
body,
|
|
3464
3661
|
hasFrontmatter
|
|
3465
3662
|
};
|
|
@@ -3508,17 +3705,34 @@ function repairFrontmatterLine(line) {
|
|
|
3508
3705
|
};
|
|
3509
3706
|
}
|
|
3510
3707
|
/**
|
|
3511
|
-
*
|
|
3512
|
-
*
|
|
3513
|
-
*
|
|
3708
|
+
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
3709
|
+
* without parsing it. Shared by the size guard in {@link parseFrontmatter} and
|
|
3710
|
+
* the YAML repair pass below, so both agree on exactly what gray-matter would
|
|
3711
|
+
* treat as the block: gray-matter ends it at the first `\n---`, with no
|
|
3712
|
+
* requirement that the delimiter be alone on its line, so a stricter pattern
|
|
3713
|
+
* here would run past gray-matter's delimiter and act on text that is really
|
|
3714
|
+
* the body.
|
|
3514
3715
|
*/
|
|
3515
|
-
function
|
|
3716
|
+
function findFrontmatterBlockBounds(content) {
|
|
3516
3717
|
const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
|
|
3517
3718
|
if (!opening) return;
|
|
3518
3719
|
const blockStart = opening[0].length;
|
|
3519
3720
|
const closing = /\r?\n---/.exec(content.slice(blockStart));
|
|
3520
3721
|
if (!closing) return;
|
|
3521
|
-
|
|
3722
|
+
return {
|
|
3723
|
+
blockStart,
|
|
3724
|
+
blockEnd: blockStart + closing.index
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* Quote the unquoted scalars that make a frontmatter block unparseable, or
|
|
3729
|
+
* return `undefined` when there is nothing to repair. Only the frontmatter
|
|
3730
|
+
* block is rewritten; the body is passed through untouched.
|
|
3731
|
+
*/
|
|
3732
|
+
function repairMalformedFrontmatterYaml(content) {
|
|
3733
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3734
|
+
if (!bounds) return;
|
|
3735
|
+
const { blockStart, blockEnd } = bounds;
|
|
3522
3736
|
const block = content.slice(blockStart, blockEnd);
|
|
3523
3737
|
const repairedLines = block.split("\n").map(repairFrontmatterLine);
|
|
3524
3738
|
const repairedBlock = repairedLines.map(({ line }) => line).join("\n");
|
|
@@ -5197,42 +5411,6 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
|
|
|
5197
5411
|
*/
|
|
5198
5412
|
const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5199
5413
|
//#endregion
|
|
5200
|
-
//#region src/utils/prototype-pollution.ts
|
|
5201
|
-
/**
|
|
5202
|
-
* Keys that, if walked into when constructing or merging objects from
|
|
5203
|
-
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
5204
|
-
* chain) and propagate state to every other object in the runtime. Any code
|
|
5205
|
-
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
5206
|
-
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
5207
|
-
*/
|
|
5208
|
-
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
5209
|
-
"__proto__",
|
|
5210
|
-
"constructor",
|
|
5211
|
-
"prototype"
|
|
5212
|
-
]);
|
|
5213
|
-
function isPrototypePollutionKey(key) {
|
|
5214
|
-
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
5215
|
-
}
|
|
5216
|
-
/**
|
|
5217
|
-
* Returns a shallow copy of a record's own entries with every
|
|
5218
|
-
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
5219
|
-
*
|
|
5220
|
-
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
5221
|
-
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
5222
|
-
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
5223
|
-
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
5224
|
-
* prototype). Walking the entries through this helper severs that path while
|
|
5225
|
-
* preserving every legitimate key.
|
|
5226
|
-
*/
|
|
5227
|
-
function omitPrototypePollutionKeys(record) {
|
|
5228
|
-
const sanitized = {};
|
|
5229
|
-
for (const [key, value] of Object.entries(record)) {
|
|
5230
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
5231
|
-
sanitized[key] = value;
|
|
5232
|
-
}
|
|
5233
|
-
return sanitized;
|
|
5234
|
-
}
|
|
5235
|
-
//#endregion
|
|
5236
5414
|
//#region src/utils/jsonc.ts
|
|
5237
5415
|
/**
|
|
5238
5416
|
* Rebuild the parsed value from its own enumerable entries, dropping
|
|
@@ -5983,6 +6161,17 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
5983
6161
|
const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
|
|
5984
6162
|
throw new RulesyncSourceNotFoundError(`No ${join(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath)} found.`);
|
|
5985
6163
|
}
|
|
6164
|
+
/**
|
|
6165
|
+
* Return one server exactly as authored, before `getMcpServers()` strips
|
|
6166
|
+
* rulesync- and tool-specific fields. Keep this lookup here so every target
|
|
6167
|
+
* that re-merges one of those fields shares the same own-property and
|
|
6168
|
+
* prototype-pollution guards.
|
|
6169
|
+
*/
|
|
6170
|
+
getRawMcpServer(name) {
|
|
6171
|
+
if (isPrototypePollutionKey(name)) return void 0;
|
|
6172
|
+
const mcpServers = isRecord$1(this.json) ? this.json.mcpServers : void 0;
|
|
6173
|
+
return isRecord$1(mcpServers) && Object.hasOwn(mcpServers, name) ? mcpServers[name] : void 0;
|
|
6174
|
+
}
|
|
5986
6175
|
getMcpServers() {
|
|
5987
6176
|
const mcpServers = this.json.mcpServers ?? {};
|
|
5988
6177
|
const entries = Object.entries(mcpServers);
|
|
@@ -7609,6 +7798,11 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
7609
7798
|
globs: z.optional(z.array(z.string())),
|
|
7610
7799
|
agentsmd: z.optional(z.looseObject({ subprojectPath: z.optional(z.string()) })),
|
|
7611
7800
|
claudecode: z.optional(z.looseObject({ paths: z.optional(z.array(z.string())) })),
|
|
7801
|
+
codebuddy: z.optional(z.looseObject({
|
|
7802
|
+
paths: z.optional(z.array(z.string())),
|
|
7803
|
+
alwaysApply: z.optional(z.boolean()),
|
|
7804
|
+
description: z.optional(z.string())
|
|
7805
|
+
})),
|
|
7612
7806
|
cursor: z.optional(z.looseObject({
|
|
7613
7807
|
alwaysApply: z.optional(z.boolean()),
|
|
7614
7808
|
description: z.optional(z.string()),
|
|
@@ -7650,7 +7844,8 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
7650
7844
|
name: z.optional(z.string()),
|
|
7651
7845
|
extends: z.optional(z.string()),
|
|
7652
7846
|
facet: z.optional(z.enum(["policies", "output-contracts"]))
|
|
7653
|
-
}))
|
|
7847
|
+
})),
|
|
7848
|
+
factorydroid: z.optional(z.looseObject({ channel: z.optional(z.enum(["design"])) }))
|
|
7654
7849
|
});
|
|
7655
7850
|
/**
|
|
7656
7851
|
* The `agentsmd.subprojectPath` every consumer should act on, resolved once so
|
|
@@ -8765,15 +8960,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8765
8960
|
})),
|
|
8766
8961
|
opencode: z.optional(z.looseObject({
|
|
8767
8962
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8768
|
-
license: z.optional(z.
|
|
8769
|
-
compatibility: z.optional(z.
|
|
8770
|
-
metadata: z.optional(z.
|
|
8963
|
+
license: z.optional(z.unknown()),
|
|
8964
|
+
compatibility: z.optional(z.unknown()),
|
|
8965
|
+
metadata: z.optional(z.unknown())
|
|
8771
8966
|
})),
|
|
8772
8967
|
kilo: z.optional(z.looseObject({
|
|
8773
8968
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8774
|
-
license: z.optional(z.
|
|
8775
|
-
compatibility: z.optional(z.
|
|
8776
|
-
metadata: z.optional(z.
|
|
8969
|
+
license: z.optional(z.unknown()),
|
|
8970
|
+
compatibility: z.optional(z.unknown()),
|
|
8971
|
+
metadata: z.optional(z.unknown())
|
|
8777
8972
|
})),
|
|
8778
8973
|
kiro: z.optional(z.looseObject({
|
|
8779
8974
|
license: z.optional(z.string()),
|
|
@@ -8782,9 +8977,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8782
8977
|
})),
|
|
8783
8978
|
deepagents: z.optional(z.looseObject({
|
|
8784
8979
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8785
|
-
license: z.optional(z.
|
|
8786
|
-
compatibility: z.optional(z.
|
|
8787
|
-
metadata: z.optional(z.
|
|
8980
|
+
license: z.optional(z.unknown()),
|
|
8981
|
+
compatibility: z.optional(z.unknown()),
|
|
8982
|
+
metadata: z.optional(z.unknown())
|
|
8788
8983
|
})),
|
|
8789
8984
|
copilot: z.optional(z.looseObject({
|
|
8790
8985
|
license: z.optional(z.string()),
|
|
@@ -8891,6 +9086,13 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8891
9086
|
takt: z.optional(z.looseObject({
|
|
8892
9087
|
name: z.optional(z.string()),
|
|
8893
9088
|
extends: z.optional(z.string())
|
|
9089
|
+
})),
|
|
9090
|
+
crush: z.optional(z.looseObject({
|
|
9091
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
9092
|
+
"user-invocable": z.optional(z.boolean()),
|
|
9093
|
+
license: z.optional(z.string()),
|
|
9094
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
9095
|
+
metadata: z.optional(z.looseObject({}))
|
|
8894
9096
|
}))
|
|
8895
9097
|
});
|
|
8896
9098
|
/**
|
|
@@ -9116,7 +9318,7 @@ async function getLocalSkillDirNames(sourceTree) {
|
|
|
9116
9318
|
*
|
|
9117
9319
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
9118
9320
|
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
9119
|
-
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9321
|
+
* copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9120
9322
|
* default with a per-target value. A defined section value (including `false`)
|
|
9121
9323
|
* always wins over the root default.
|
|
9122
9324
|
*
|
|
@@ -9135,7 +9337,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
9135
9337
|
*
|
|
9136
9338
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
9137
9339
|
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
9138
|
-
* copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9340
|
+
* copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9139
9341
|
* per-target value. A defined section value (including `false`) always wins
|
|
9140
9342
|
* over the root default.
|
|
9141
9343
|
*
|
|
@@ -9600,8 +9802,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
9600
9802
|
* This only deletes files that are no longer in the rulesync source, not files that will be overwritten.
|
|
9601
9803
|
*/
|
|
9602
9804
|
async removeOrphanAiFiles(existingFiles, generatedFiles) {
|
|
9603
|
-
const generatedPaths = new Set(generatedFiles.map((f) => f.getFilePath()));
|
|
9604
|
-
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(f.getFilePath()));
|
|
9805
|
+
const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
|
|
9806
|
+
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
|
|
9605
9807
|
for (const aiFile of orphanFiles) {
|
|
9606
9808
|
const filePath = aiFile.getFilePath();
|
|
9607
9809
|
const loggedPath = stripControlCharacters(filePath);
|
|
@@ -10728,6 +10930,15 @@ const FACTORYDROID_COMMANDS_DIR_PATH = join(FACTORYDROID_DIR, "commands");
|
|
|
10728
10930
|
const FACTORYDROID_SKILLS_DIR_PATH = join(FACTORYDROID_DIR, "skills");
|
|
10729
10931
|
const FACTORYDROID_DROIDS_DIR_PATH = join(FACTORYDROID_DIR, "droids");
|
|
10730
10932
|
const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
|
|
10933
|
+
/**
|
|
10934
|
+
* Factory Droid's design-guidelines instruction file: "Always-on design-system,
|
|
10935
|
+
* UX, visual, and interaction guidance", loaded separately from `AGENTS.md`'s
|
|
10936
|
+
* coding guidelines. Project scope only — Factory's docs describe root and
|
|
10937
|
+
* nested `DESIGN.md` files like `AGENTS.md`, but document no personal/global
|
|
10938
|
+
* home-directory equivalent.
|
|
10939
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
10940
|
+
*/
|
|
10941
|
+
const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
|
|
10731
10942
|
const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
|
|
10732
10943
|
const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
|
|
10733
10944
|
const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
@@ -11250,6 +11461,21 @@ function stripStrings(_key, value) {
|
|
|
11250
11461
|
//#endregion
|
|
11251
11462
|
//#region src/features/shared/shared-config-gateway.ts
|
|
11252
11463
|
/**
|
|
11464
|
+
* Upper bound on the number of values a shared config document may expand
|
|
11465
|
+
* to once every YAML alias is written out. Real config files hold a few
|
|
11466
|
+
* hundred values at most; even a large MCP server catalog stays orders of
|
|
11467
|
+
* magnitude below the limit.
|
|
11468
|
+
*/
|
|
11469
|
+
const MAX_SHARED_CONFIG_VALUES = 1e5;
|
|
11470
|
+
/**
|
|
11471
|
+
* Upper bound on the total character count of the string leaves and keys a
|
|
11472
|
+
* shared config document may expand to. The value budget bounds how many
|
|
11473
|
+
* values are visited, but one long string aliased thousands of times fits
|
|
11474
|
+
* that budget while the duplicated output balloons; charging every visited
|
|
11475
|
+
* string's length separately bounds the output regardless of alias count.
|
|
11476
|
+
*/
|
|
11477
|
+
const MAX_SHARED_CONFIG_STRING_CHARS = 4e6;
|
|
11478
|
+
/**
|
|
11253
11479
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
11254
11480
|
*
|
|
11255
11481
|
* Every object is rebuilt, not just the ones that are already plain: a literal
|
|
@@ -11264,15 +11490,61 @@ function stripStrings(_key, value) {
|
|
|
11264
11490
|
*
|
|
11265
11491
|
* Dates are the one object the YAML and TOML parsers produce that is not a
|
|
11266
11492
|
* mapping, so they are passed through rather than flattened into `{}`.
|
|
11493
|
+
*
|
|
11494
|
+
* The rebuild is bounded, because a YAML alias makes one parsed container
|
|
11495
|
+
* reachable from many keys and every alias is copied out independently (the
|
|
11496
|
+
* writers dump with `noRefs: true`, so memoizing here would only move the
|
|
11497
|
+
* blowup into serialization). A small "alias bomb" of nested anchors would
|
|
11498
|
+
* otherwise cost exponential time and memory, and a self-referencing anchor
|
|
11499
|
+
* would recurse until the stack overflowed — both reachable from a config
|
|
11500
|
+
* file committed to a cloned repository. The walk therefore charges every
|
|
11501
|
+
* value against {@link MAX_SHARED_CONFIG_VALUES}, every string and key
|
|
11502
|
+
* against {@link MAX_SHARED_CONFIG_STRING_CHARS}, caps nesting at
|
|
11503
|
+
* {@link MAX_SHARED_CONFIG_DEPTH}, and refuses a reference back to an
|
|
11504
|
+
* ancestor outright, each with a clear error instead of a hang or a crash.
|
|
11267
11505
|
*/
|
|
11268
11506
|
function sanitizeSharedConfigValue(value) {
|
|
11269
|
-
|
|
11507
|
+
return sanitizeSharedConfigValueBounded(value, createBoundedWalk({
|
|
11508
|
+
subject: "Shared config",
|
|
11509
|
+
limits: {
|
|
11510
|
+
maxValues: MAX_SHARED_CONFIG_VALUES,
|
|
11511
|
+
maxStringChars: MAX_SHARED_CONFIG_STRING_CHARS,
|
|
11512
|
+
maxDepth: 64
|
|
11513
|
+
}
|
|
11514
|
+
}));
|
|
11515
|
+
}
|
|
11516
|
+
/**
|
|
11517
|
+
* Refuse a container that is already on the descent path. Unlike the
|
|
11518
|
+
* frontmatter cleaner, which drops such a cycle and keeps the rest of the
|
|
11519
|
+
* document, a shared config file is refused outright: silently dropping part
|
|
11520
|
+
* of a user's settings file would let a later write-back persist the loss.
|
|
11521
|
+
*/
|
|
11522
|
+
function enterSharedConfigContainer(walk, container) {
|
|
11523
|
+
if (walk.isAncestor(container)) throw new Error("Shared config contains a value that refers back to itself (a circular YAML alias); refusing to process it");
|
|
11524
|
+
walk.enter(container);
|
|
11525
|
+
}
|
|
11526
|
+
function sanitizeSharedConfigValueBounded(value, walk) {
|
|
11527
|
+
if (typeof value === "string") {
|
|
11528
|
+
walk.chargeValue(value.length);
|
|
11529
|
+
return value;
|
|
11530
|
+
}
|
|
11531
|
+
walk.chargeValue();
|
|
11532
|
+
if (Array.isArray(value)) {
|
|
11533
|
+
enterSharedConfigContainer(walk, value);
|
|
11534
|
+
const items = value.map((item) => sanitizeSharedConfigValueBounded(item, walk));
|
|
11535
|
+
walk.leave(value);
|
|
11536
|
+
return items;
|
|
11537
|
+
}
|
|
11270
11538
|
if (value === null || typeof value !== "object" || value instanceof Date) return value;
|
|
11539
|
+
enterSharedConfigContainer(walk, value);
|
|
11271
11540
|
const result = {};
|
|
11272
11541
|
for (const [key, nested] of Object.entries(value)) {
|
|
11542
|
+
walk.chargeChars(key.length);
|
|
11543
|
+
const sanitized = sanitizeSharedConfigValueBounded(nested, walk);
|
|
11273
11544
|
if (isPrototypePollutionKey(key)) continue;
|
|
11274
|
-
result[key] =
|
|
11545
|
+
result[key] = sanitized;
|
|
11275
11546
|
}
|
|
11547
|
+
walk.leave(value);
|
|
11276
11548
|
return result;
|
|
11277
11549
|
}
|
|
11278
11550
|
/**
|
|
@@ -11301,7 +11573,12 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
11301
11573
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11302
11574
|
}
|
|
11303
11575
|
if (parsed === void 0 || parsed === null) return {};
|
|
11304
|
-
|
|
11576
|
+
let sanitized;
|
|
11577
|
+
try {
|
|
11578
|
+
sanitized = sanitizeSharedConfigValue(parsed);
|
|
11579
|
+
} catch (error) {
|
|
11580
|
+
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11581
|
+
}
|
|
11305
11582
|
if (!isPlainObject$1(sanitized)) {
|
|
11306
11583
|
if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
|
|
11307
11584
|
return {};
|
|
@@ -15627,6 +15904,10 @@ function toAllowedToolsArray(value) {
|
|
|
15627
15904
|
* The spec types `compatibility` as a free-form string. An object from a legacy
|
|
15628
15905
|
* rulesync input is flattened to `key: value` pairs instead of being emitted as
|
|
15629
15906
|
* a YAML mapping, which conformant clients reject.
|
|
15907
|
+
*
|
|
15908
|
+
* Exported for `CrushSkill`, which requires the same bare-string shape (Crush's
|
|
15909
|
+
* Go struct types `Compatibility` as a plain `string`) and reuses this
|
|
15910
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15630
15911
|
*/
|
|
15631
15912
|
function toCompatibilityString(value) {
|
|
15632
15913
|
if (typeof value === "string") return value;
|
|
@@ -15635,6 +15916,10 @@ function toCompatibilityString(value) {
|
|
|
15635
15916
|
/**
|
|
15636
15917
|
* The spec types `metadata` as "a map from string keys to string values", so
|
|
15637
15918
|
* non-string values (e.g. a YAML number `version: 1`) are stringified.
|
|
15919
|
+
*
|
|
15920
|
+
* Exported for `CrushSkill`, which requires the same `map[string]string`
|
|
15921
|
+
* shape (Crush's Go struct types `Metadata` that way) and reuses this
|
|
15922
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15638
15923
|
*/
|
|
15639
15924
|
function toStringMetadata(metadata) {
|
|
15640
15925
|
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
|
|
@@ -18645,7 +18930,13 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18645
18930
|
if (!matchByBasename || flatOnly && dirname(key) !== ".") return [key];
|
|
18646
18931
|
return [key, basename(key)];
|
|
18647
18932
|
};
|
|
18648
|
-
const
|
|
18933
|
+
const claimedKeys = new ClaimedIdentities();
|
|
18934
|
+
const primarySource = paths.relativeDirPath;
|
|
18935
|
+
const secondarySource = "a secondary source";
|
|
18936
|
+
for (const command of toolCommands) for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18937
|
+
identity: candidate,
|
|
18938
|
+
source: primarySource
|
|
18939
|
+
});
|
|
18649
18940
|
const additionalCommands = await factory.class.loadAdditionalImportFiles({
|
|
18650
18941
|
outputRoot: this.outputRoot,
|
|
18651
18942
|
global: this.global,
|
|
@@ -18653,11 +18944,26 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18653
18944
|
});
|
|
18654
18945
|
for (const command of additionalCommands) {
|
|
18655
18946
|
const key = command.getRelativeFilePath();
|
|
18656
|
-
|
|
18657
|
-
|
|
18947
|
+
const collision = [...new Set(keysOf(command, true))].map((candidate) => {
|
|
18948
|
+
const claimed = claimedKeys.claim({
|
|
18949
|
+
identity: candidate,
|
|
18950
|
+
source: secondarySource
|
|
18951
|
+
});
|
|
18952
|
+
return claimed === null ? void 0 : {
|
|
18953
|
+
candidate,
|
|
18954
|
+
claimed
|
|
18955
|
+
};
|
|
18956
|
+
}).find((hit) => hit !== void 0);
|
|
18957
|
+
if (collision) {
|
|
18958
|
+
const { candidate, claimed } = collision;
|
|
18959
|
+
if (claimed.spelling === candidate) this.logger.warn(`Duplicate ${this.toolTarget} command "${stripControlCharacters(key)}" from ${secondarySource}; keeping the one already loaded.`);
|
|
18960
|
+
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.`);
|
|
18658
18961
|
continue;
|
|
18659
18962
|
}
|
|
18660
|
-
for (const candidate of keysOf(command))
|
|
18963
|
+
for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18964
|
+
identity: candidate,
|
|
18965
|
+
source: secondarySource
|
|
18966
|
+
});
|
|
18661
18967
|
toolCommands.push(command);
|
|
18662
18968
|
}
|
|
18663
18969
|
}
|
|
@@ -18947,6 +19253,21 @@ var AmpHooks = class AmpHooks extends ToolHooks {
|
|
|
18947
19253
|
}
|
|
18948
19254
|
};
|
|
18949
19255
|
//#endregion
|
|
19256
|
+
//#region src/utils/own-lookup.ts
|
|
19257
|
+
/**
|
|
19258
|
+
* Read a key from a plain string map without walking its prototype chain.
|
|
19259
|
+
*
|
|
19260
|
+
* A bracket read on an object literal resolves inherited members too, so a
|
|
19261
|
+
* user-supplied key such as `toString` or `constructor` "succeeds" with an
|
|
19262
|
+
* `Object.prototype` function instead of falling through to the caller's
|
|
19263
|
+
* `?? fallback`. Hook adapters translate native event names this way from
|
|
19264
|
+
* `Object.entries()` over a config file, so route the read through here to keep
|
|
19265
|
+
* the fallback honest: only a key the map itself defines yields a value.
|
|
19266
|
+
*/
|
|
19267
|
+
function lookupOwn({ record, key }) {
|
|
19268
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
19269
|
+
}
|
|
19270
|
+
//#endregion
|
|
18950
19271
|
//#region src/utils/object.ts
|
|
18951
19272
|
/**
|
|
18952
19273
|
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
@@ -19331,7 +19652,10 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
19331
19652
|
const warn = warnOnce(logger);
|
|
19332
19653
|
const result = {};
|
|
19333
19654
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
19334
|
-
const toolEventName =
|
|
19655
|
+
const toolEventName = lookupOwn({
|
|
19656
|
+
record: converterConfig.canonicalToToolEventNames,
|
|
19657
|
+
key: eventName
|
|
19658
|
+
}) ?? eventName;
|
|
19335
19659
|
const byMatcher = groupDefinitionsByMatcher({
|
|
19336
19660
|
definitions,
|
|
19337
19661
|
converterConfig
|
|
@@ -19759,7 +20083,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
19759
20083
|
const warn = warnOnce(logger);
|
|
19760
20084
|
const canonical = {};
|
|
19761
20085
|
for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
|
|
19762
|
-
const eventName =
|
|
20086
|
+
const eventName = lookupOwn({
|
|
20087
|
+
record: converterConfig.toolToCanonicalEventNames,
|
|
20088
|
+
key: toolEventName
|
|
20089
|
+
}) ?? toolEventName;
|
|
19763
20090
|
if (!Array.isArray(matcherEntries)) continue;
|
|
19764
20091
|
const defs = [];
|
|
19765
20092
|
for (const rawEntry of matcherEntries) {
|
|
@@ -19815,7 +20142,10 @@ function flattenAntigravityHooks(parsed) {
|
|
|
19815
20142
|
const flat = {};
|
|
19816
20143
|
const addEvent = (event, entries) => {
|
|
19817
20144
|
if (isPrototypePollutionKey(event) || !Array.isArray(entries)) return;
|
|
19818
|
-
const existing =
|
|
20145
|
+
const existing = lookupOwn({
|
|
20146
|
+
record: flat,
|
|
20147
|
+
key: event
|
|
20148
|
+
});
|
|
19819
20149
|
flat[event] = existing ? [...existing, ...entries] : [...entries];
|
|
19820
20150
|
};
|
|
19821
20151
|
for (const [key, value] of Object.entries(parsed)) if (Array.isArray(value)) addEvent(key, value);
|
|
@@ -20202,6 +20532,16 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20202
20532
|
const paths = AugmentcodeHooks.getSettablePaths({ global });
|
|
20203
20533
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
20204
20534
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
20535
|
+
let existingHooks = {};
|
|
20536
|
+
try {
|
|
20537
|
+
const parsed = JSON.parse(existingContent);
|
|
20538
|
+
const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
|
|
20539
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
|
|
20540
|
+
} catch {
|
|
20541
|
+
existingHooks = {};
|
|
20542
|
+
}
|
|
20543
|
+
const nativeEventKeys = new Set(Object.values(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES));
|
|
20544
|
+
const preservedHooks = Object.fromEntries(Object.entries(existingHooks).filter(([key]) => !nativeEventKeys.has(key)));
|
|
20205
20545
|
const config = rulesyncHooks.getJson();
|
|
20206
20546
|
const augmentHooks = canonicalToToolHooks({
|
|
20207
20547
|
config,
|
|
@@ -20213,7 +20553,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20213
20553
|
fileKey: sharedConfigFileKey(paths),
|
|
20214
20554
|
feature: "hooks",
|
|
20215
20555
|
existingContent,
|
|
20216
|
-
patch: { hooks:
|
|
20556
|
+
patch: { hooks: {
|
|
20557
|
+
...preservedHooks,
|
|
20558
|
+
...augmentHooks
|
|
20559
|
+
} },
|
|
20217
20560
|
filePath
|
|
20218
20561
|
});
|
|
20219
20562
|
return new AugmentcodeHooks({
|
|
@@ -20971,7 +21314,10 @@ function canonicalToCopilotHooks(config) {
|
|
|
20971
21314
|
};
|
|
20972
21315
|
const copilot = {};
|
|
20973
21316
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
20974
|
-
const copilotEventName =
|
|
21317
|
+
const copilotEventName = lookupOwn({
|
|
21318
|
+
record: CANONICAL_TO_COPILOT_EVENT_NAMES,
|
|
21319
|
+
key: eventName
|
|
21320
|
+
}) ?? eventName;
|
|
20975
21321
|
const entries = [];
|
|
20976
21322
|
for (const def of definitions) {
|
|
20977
21323
|
const hookType = def.type ?? "command";
|
|
@@ -21048,7 +21394,10 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
21048
21394
|
if (copilotHooks === null || copilotHooks === void 0 || typeof copilotHooks !== "object") return {};
|
|
21049
21395
|
const canonical = {};
|
|
21050
21396
|
for (const [copilotEventName, hookEntries] of Object.entries(copilotHooks)) {
|
|
21051
|
-
const eventName =
|
|
21397
|
+
const eventName = lookupOwn({
|
|
21398
|
+
record: COPILOT_TO_CANONICAL_EVENT_NAMES,
|
|
21399
|
+
key: copilotEventName
|
|
21400
|
+
}) ?? copilotEventName;
|
|
21052
21401
|
if (!Array.isArray(hookEntries)) continue;
|
|
21053
21402
|
const defs = [];
|
|
21054
21403
|
for (const rawEntry of hookEntries) {
|
|
@@ -21318,7 +21667,10 @@ function canonicalToCopilotCliHooks(config, logger) {
|
|
|
21318
21667
|
};
|
|
21319
21668
|
const out = {};
|
|
21320
21669
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
21321
|
-
const copilotEventName =
|
|
21670
|
+
const copilotEventName = lookupOwn({
|
|
21671
|
+
record: CANONICAL_TO_COPILOTCLI_EVENT_NAMES,
|
|
21672
|
+
key: eventName
|
|
21673
|
+
}) ?? eventName;
|
|
21322
21674
|
const entries = buildCopilotCliEntriesForEvent({
|
|
21323
21675
|
eventName,
|
|
21324
21676
|
definitions,
|
|
@@ -21375,7 +21727,10 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
|
|
|
21375
21727
|
if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
|
|
21376
21728
|
const canonical = {};
|
|
21377
21729
|
for (const [copilotEventName, hookEntries] of Object.entries(rawHooks)) {
|
|
21378
|
-
const eventName =
|
|
21730
|
+
const eventName = lookupOwn({
|
|
21731
|
+
record: COPILOTCLI_TO_CANONICAL_EVENT_NAMES,
|
|
21732
|
+
key: copilotEventName
|
|
21733
|
+
}) ?? copilotEventName;
|
|
21379
21734
|
if (!Array.isArray(hookEntries)) continue;
|
|
21380
21735
|
const defs = [];
|
|
21381
21736
|
for (const rawEntry of hookEntries) {
|
|
@@ -21527,7 +21882,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21527
21882
|
const mappedHooks = {};
|
|
21528
21883
|
const cursorSupportedTypes = /* @__PURE__ */ new Set(["command", "prompt"]);
|
|
21529
21884
|
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
21530
|
-
const cursorEventName =
|
|
21885
|
+
const cursorEventName = lookupOwn({
|
|
21886
|
+
record: CANONICAL_TO_CURSOR_EVENT_NAMES,
|
|
21887
|
+
key: eventName
|
|
21888
|
+
}) ?? eventName;
|
|
21531
21889
|
const mappedDefs = defs.filter((def) => cursorSupportedTypes.has(def.type ?? "command")).map((def) => ({
|
|
21532
21890
|
...def.type !== void 0 && def.type !== null && { type: def.type },
|
|
21533
21891
|
...def.command !== void 0 && def.command !== null && { command: def.command },
|
|
@@ -21560,7 +21918,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21560
21918
|
const cursorHooks = parsed.hooks ?? {};
|
|
21561
21919
|
const canonicalHooks = {};
|
|
21562
21920
|
for (const [cursorEventName, defs] of Object.entries(cursorHooks)) {
|
|
21563
|
-
const eventName =
|
|
21921
|
+
const eventName = lookupOwn({
|
|
21922
|
+
record: CURSOR_TO_CANONICAL_EVENT_NAMES,
|
|
21923
|
+
key: cursorEventName
|
|
21924
|
+
}) ?? cursorEventName;
|
|
21564
21925
|
canonicalHooks[eventName] = defs;
|
|
21565
21926
|
}
|
|
21566
21927
|
const version = parsed.version ?? 1;
|
|
@@ -21630,7 +21991,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21630
21991
|
const hooks = {};
|
|
21631
21992
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
21632
21993
|
if (!supported.has(canonicalEvent)) continue;
|
|
21633
|
-
const deepagentsEvent =
|
|
21994
|
+
const deepagentsEvent = lookupOwn({
|
|
21995
|
+
record: CANONICAL_TO_DEEPAGENTS_EVENT_NAMES,
|
|
21996
|
+
key: canonicalEvent
|
|
21997
|
+
});
|
|
21634
21998
|
if (!deepagentsEvent) continue;
|
|
21635
21999
|
for (const def of definitions) {
|
|
21636
22000
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -21659,7 +22023,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21659
22023
|
function deepagentsToCanonicalHooks(hooks) {
|
|
21660
22024
|
const canonical = {};
|
|
21661
22025
|
for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
|
|
21662
|
-
const canonicalEvent =
|
|
22026
|
+
const canonicalEvent = lookupOwn({
|
|
22027
|
+
record: DEEPAGENTS_TO_CANONICAL_EVENT_NAMES,
|
|
22028
|
+
key: deepagentsEvent
|
|
22029
|
+
});
|
|
21663
22030
|
if (!canonicalEvent || !Array.isArray(groups)) continue;
|
|
21664
22031
|
for (const group of groups) {
|
|
21665
22032
|
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
@@ -21692,7 +22059,10 @@ function deepagentsLegacyToCanonicalHooks(entries) {
|
|
|
21692
22059
|
const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
|
|
21693
22060
|
const events = Array.isArray(entry.events) ? entry.events : [];
|
|
21694
22061
|
for (const legacyEvent of events) {
|
|
21695
|
-
const canonicalEvent = typeof legacyEvent === "string" ?
|
|
22062
|
+
const canonicalEvent = typeof legacyEvent === "string" ? lookupOwn({
|
|
22063
|
+
record: DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES,
|
|
22064
|
+
key: legacyEvent
|
|
22065
|
+
}) : void 0;
|
|
21696
22066
|
if (!canonicalEvent) continue;
|
|
21697
22067
|
(canonical[canonicalEvent] ??= []).push({
|
|
21698
22068
|
type: "command",
|
|
@@ -22413,7 +22783,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22413
22783
|
const result = {};
|
|
22414
22784
|
for (const [canonicalEvent, definitions] of Object.entries(config.hooks)) {
|
|
22415
22785
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22416
|
-
const nativeEvent =
|
|
22786
|
+
const nativeEvent = lookupOwn({
|
|
22787
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22788
|
+
key: canonicalEvent
|
|
22789
|
+
});
|
|
22417
22790
|
if (nativeEvent) setHermesHookEntries({
|
|
22418
22791
|
result,
|
|
22419
22792
|
event: nativeEvent,
|
|
@@ -22424,7 +22797,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22424
22797
|
}
|
|
22425
22798
|
for (const [canonicalEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
22426
22799
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22427
|
-
const nativeEvent =
|
|
22800
|
+
const nativeEvent = lookupOwn({
|
|
22801
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22802
|
+
key: canonicalEvent
|
|
22803
|
+
});
|
|
22428
22804
|
if (nativeEvent) setHermesHookEntries({
|
|
22429
22805
|
result,
|
|
22430
22806
|
event: nativeEvent,
|
|
@@ -22490,7 +22866,10 @@ function hermesHooksToCanonical(hooks) {
|
|
|
22490
22866
|
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
22491
22867
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
22492
22868
|
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
22493
|
-
const rulesyncEvent =
|
|
22869
|
+
const rulesyncEvent = lookupOwn({
|
|
22870
|
+
record: HERMESAGENT_TO_CANONICAL_EVENT_NAMES,
|
|
22871
|
+
key: nativeEvent
|
|
22872
|
+
}) ?? nativeEvent;
|
|
22494
22873
|
const defs = entries.map((raw) => hermesEntryToDefinition({
|
|
22495
22874
|
nativeEvent,
|
|
22496
22875
|
raw
|
|
@@ -23127,7 +23506,10 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
|
|
|
23127
23506
|
const result = [];
|
|
23128
23507
|
const nativeEvents = new Set(KIMI_CODE_NATIVE_HOOK_EVENTS);
|
|
23129
23508
|
for (const [event, definitions] of Object.entries(buildEffectiveHooks(config, toolOverrideHooks))) {
|
|
23130
|
-
const nativeEvent =
|
|
23509
|
+
const nativeEvent = lookupOwn({
|
|
23510
|
+
record: CANONICAL_TO_KIMI_CODE_EVENT_NAMES,
|
|
23511
|
+
key: event
|
|
23512
|
+
}) ?? event;
|
|
23131
23513
|
if (!nativeEvents.has(nativeEvent)) {
|
|
23132
23514
|
logger?.warn(`Kimi Code hooks: skipping unsupported event "${event}".`);
|
|
23133
23515
|
continue;
|
|
@@ -23161,14 +23543,22 @@ function kimiCodeHooksToCanonical(hooks) {
|
|
|
23161
23543
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
23162
23544
|
const entry = raw;
|
|
23163
23545
|
if (typeof entry.event !== "string" || typeof entry.command !== "string") continue;
|
|
23164
|
-
const event =
|
|
23546
|
+
const event = lookupOwn({
|
|
23547
|
+
record: KIMI_CODE_TO_CANONICAL_EVENT_NAMES,
|
|
23548
|
+
key: entry.event
|
|
23549
|
+
}) ?? entry.event;
|
|
23165
23550
|
const definition = {
|
|
23166
23551
|
type: "command",
|
|
23167
23552
|
command: stripTrustedDirectoryWrapper(entry.command),
|
|
23168
23553
|
...typeof entry.matcher === "string" && { matcher: entry.matcher },
|
|
23169
23554
|
...typeof entry.timeout === "number" && { timeout: entry.timeout }
|
|
23170
23555
|
};
|
|
23171
|
-
|
|
23556
|
+
const list = lookupOwn({
|
|
23557
|
+
record: result,
|
|
23558
|
+
key: event
|
|
23559
|
+
}) ?? [];
|
|
23560
|
+
list.push(definition);
|
|
23561
|
+
result[event] = list;
|
|
23172
23562
|
}
|
|
23173
23563
|
return result;
|
|
23174
23564
|
}
|
|
@@ -23371,7 +23761,13 @@ function canonicalToKiroIdeHooks(config) {
|
|
|
23371
23761
|
};
|
|
23372
23762
|
const entries = [];
|
|
23373
23763
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23374
|
-
const trigger =
|
|
23764
|
+
const trigger = lookupOwn({
|
|
23765
|
+
record: CANONICAL_TO_KIRO_IDE_EVENT_NAMES,
|
|
23766
|
+
key: eventName
|
|
23767
|
+
}) ?? lookupOwn({
|
|
23768
|
+
record: KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES,
|
|
23769
|
+
key: eventName
|
|
23770
|
+
}) ?? eventName;
|
|
23375
23771
|
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
23376
23772
|
}
|
|
23377
23773
|
return entries;
|
|
@@ -23380,7 +23776,10 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23380
23776
|
const canonical = {};
|
|
23381
23777
|
for (const entry of entries) {
|
|
23382
23778
|
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
23383
|
-
const eventName =
|
|
23779
|
+
const eventName = lookupOwn({
|
|
23780
|
+
record: KIRO_IDE_TO_CANONICAL_EVENT_NAMES,
|
|
23781
|
+
key: entry.trigger
|
|
23782
|
+
}) ?? entry.trigger;
|
|
23384
23783
|
if (isPrototypePollutionKey(eventName)) continue;
|
|
23385
23784
|
const def = {};
|
|
23386
23785
|
if (entry.action.type === "command") {
|
|
@@ -23397,7 +23796,12 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23397
23796
|
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
23398
23797
|
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
23399
23798
|
if (entry.enabled === false) def.enabled = false;
|
|
23400
|
-
|
|
23799
|
+
const list = lookupOwn({
|
|
23800
|
+
record: canonical,
|
|
23801
|
+
key: eventName
|
|
23802
|
+
}) ?? [];
|
|
23803
|
+
list.push(def);
|
|
23804
|
+
canonical[eventName] = list;
|
|
23401
23805
|
}
|
|
23402
23806
|
return canonical;
|
|
23403
23807
|
}
|
|
@@ -23578,7 +23982,10 @@ function canonicalToKiroHooks({ config, logger }) {
|
|
|
23578
23982
|
};
|
|
23579
23983
|
const kiro = {};
|
|
23580
23984
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23581
|
-
const kiroEventName =
|
|
23985
|
+
const kiroEventName = lookupOwn({
|
|
23986
|
+
record: CANONICAL_TO_KIRO_EVENT_NAMES,
|
|
23987
|
+
key: eventName
|
|
23988
|
+
}) ?? eventName;
|
|
23582
23989
|
const entries = buildKiroEntriesForEvent(definitions);
|
|
23583
23990
|
if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
|
|
23584
23991
|
else kiro[kiroEventName] = entries;
|
|
@@ -23609,7 +24016,10 @@ function kiroHooksToCanonical(kiroHooks) {
|
|
|
23609
24016
|
if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
|
|
23610
24017
|
const canonical = {};
|
|
23611
24018
|
for (const [kiroEventName, entries] of Object.entries(kiroHooks)) {
|
|
23612
|
-
const eventName =
|
|
24019
|
+
const eventName = lookupOwn({
|
|
24020
|
+
record: KIRO_TO_CANONICAL_EVENT_NAMES,
|
|
24021
|
+
key: kiroEventName
|
|
24022
|
+
}) ?? kiroEventName;
|
|
23613
24023
|
if (!Array.isArray(entries)) continue;
|
|
23614
24024
|
const defs = [];
|
|
23615
24025
|
for (const rawEntry of entries) {
|
|
@@ -24168,7 +24578,10 @@ function canonicalToQwencodeHooks(config, logger) {
|
|
|
24168
24578
|
]);
|
|
24169
24579
|
const qwencode = {};
|
|
24170
24580
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
24171
|
-
const qwencodeEventName =
|
|
24581
|
+
const qwencodeEventName = lookupOwn({
|
|
24582
|
+
record: CANONICAL_TO_QWENCODE_EVENT_NAMES,
|
|
24583
|
+
key: eventName
|
|
24584
|
+
}) ?? eventName;
|
|
24172
24585
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
24173
24586
|
for (const def of definitions) {
|
|
24174
24587
|
if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
|
|
@@ -24273,7 +24686,10 @@ function qwencodeHooksToCanonical(qwencodeHooks) {
|
|
|
24273
24686
|
if (qwencodeHooks === null || qwencodeHooks === void 0 || typeof qwencodeHooks !== "object") return {};
|
|
24274
24687
|
const canonical = {};
|
|
24275
24688
|
for (const [qwencodeEventName, matcherEntries] of Object.entries(qwencodeHooks)) {
|
|
24276
|
-
const eventName =
|
|
24689
|
+
const eventName = lookupOwn({
|
|
24690
|
+
record: QWENCODE_TO_CANONICAL_EVENT_NAMES,
|
|
24691
|
+
key: qwencodeEventName
|
|
24692
|
+
}) ?? qwencodeEventName;
|
|
24277
24693
|
if (!Array.isArray(matcherEntries)) continue;
|
|
24278
24694
|
const defs = [];
|
|
24279
24695
|
for (const rawEntry of matcherEntries) {
|
|
@@ -24388,7 +24804,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24388
24804
|
const result = {};
|
|
24389
24805
|
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
24390
24806
|
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
24391
|
-
const reasonixEvent =
|
|
24807
|
+
const reasonixEvent = lookupOwn({
|
|
24808
|
+
record: CANONICAL_TO_REASONIX_EVENT_NAMES,
|
|
24809
|
+
key: event
|
|
24810
|
+
}) ?? event;
|
|
24392
24811
|
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
24393
24812
|
const entries = [];
|
|
24394
24813
|
for (const def of defs) {
|
|
@@ -24401,7 +24820,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24401
24820
|
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
24402
24821
|
entries.push(entry);
|
|
24403
24822
|
}
|
|
24404
|
-
if (entries.length > 0) result[reasonixEvent] = [...
|
|
24823
|
+
if (entries.length > 0) result[reasonixEvent] = [...lookupOwn({
|
|
24824
|
+
record: result,
|
|
24825
|
+
key: reasonixEvent
|
|
24826
|
+
}) ?? [], ...entries];
|
|
24405
24827
|
}
|
|
24406
24828
|
return result;
|
|
24407
24829
|
}
|
|
@@ -24414,7 +24836,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24414
24836
|
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
24415
24837
|
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
24416
24838
|
if (!Array.isArray(rawEntries)) continue;
|
|
24417
|
-
const canonicalEvent =
|
|
24839
|
+
const canonicalEvent = lookupOwn({
|
|
24840
|
+
record: REASONIX_TO_CANONICAL_EVENT_NAMES,
|
|
24841
|
+
key: reasonixEvent
|
|
24842
|
+
}) ?? reasonixEvent;
|
|
24418
24843
|
const defs = [];
|
|
24419
24844
|
for (const rawEntry of rawEntries) {
|
|
24420
24845
|
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
@@ -24429,7 +24854,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24429
24854
|
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
24430
24855
|
defs.push(def);
|
|
24431
24856
|
}
|
|
24432
|
-
if (defs.length > 0) canonical[canonicalEvent] = [...
|
|
24857
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...lookupOwn({
|
|
24858
|
+
record: canonical,
|
|
24859
|
+
key: canonicalEvent
|
|
24860
|
+
}) ?? [], ...defs];
|
|
24433
24861
|
}
|
|
24434
24862
|
return canonical;
|
|
24435
24863
|
}
|
|
@@ -24557,7 +24985,10 @@ function canonicalToVibeHooks(config, toolOverride) {
|
|
|
24557
24985
|
const hooks = [];
|
|
24558
24986
|
for (const [event, defs] of Object.entries(effective)) {
|
|
24559
24987
|
if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
|
|
24560
|
-
const vibeEvent =
|
|
24988
|
+
const vibeEvent = lookupOwn({
|
|
24989
|
+
record: CANONICAL_TO_VIBE_EVENT_NAMES,
|
|
24990
|
+
key: event
|
|
24991
|
+
}) ?? event;
|
|
24561
24992
|
let index = 0;
|
|
24562
24993
|
for (const def of defs) {
|
|
24563
24994
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -24587,7 +25018,10 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
24587
25018
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
24588
25019
|
if (vibeEvent === void 0) return null;
|
|
24589
25020
|
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
24590
|
-
const canonicalEvent =
|
|
25021
|
+
const canonicalEvent = lookupOwn({
|
|
25022
|
+
record: VIBE_TO_CANONICAL_EVENT_NAMES,
|
|
25023
|
+
key: vibeEvent
|
|
25024
|
+
}) ?? vibeEvent;
|
|
24591
25025
|
const def = { type: "command" };
|
|
24592
25026
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
24593
25027
|
if (typeof entry.match === "string" && entry.match !== "" && entry.match !== "*") def.matcher = entry.match;
|
|
@@ -24612,7 +25046,10 @@ function vibeHooksToCanonical(parsed) {
|
|
|
24612
25046
|
for (const raw of rawHooks) {
|
|
24613
25047
|
const result = vibeEntryToCanonicalDef(raw);
|
|
24614
25048
|
if (result === null) continue;
|
|
24615
|
-
const list =
|
|
25049
|
+
const list = lookupOwn({
|
|
25050
|
+
record: canonical,
|
|
25051
|
+
key: result.canonicalEvent
|
|
25052
|
+
}) ?? [];
|
|
24616
25053
|
list.push(result.def);
|
|
24617
25054
|
canonical[result.canonicalEvent] = list;
|
|
24618
25055
|
}
|
|
@@ -25691,6 +26128,68 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
|
|
|
25691
26128
|
}
|
|
25692
26129
|
};
|
|
25693
26130
|
//#endregion
|
|
26131
|
+
//#region src/constants/crush-paths.ts
|
|
26132
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
26133
|
+
const CRUSH_GLOBAL_DIR = join(".config", "crush");
|
|
26134
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
26135
|
+
const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
|
|
26136
|
+
const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
|
|
26137
|
+
//#endregion
|
|
26138
|
+
//#region src/features/ignore/crush-ignore.ts
|
|
26139
|
+
/**
|
|
26140
|
+
* Ignore generator for Crush.
|
|
26141
|
+
*
|
|
26142
|
+
* Crush excludes files from tool access via a `.crushignore` file, read
|
|
26143
|
+
* hierarchically (root and any subdirectory, the same way it walks
|
|
26144
|
+
* `.gitignore`) using gitignore syntax. Crush documents no global/user-scope
|
|
26145
|
+
* ignore file, so this is project-only.
|
|
26146
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/fsext/fileutil.go
|
|
26147
|
+
*/
|
|
26148
|
+
var CrushIgnore = class CrushIgnore extends ToolIgnore {
|
|
26149
|
+
static getSettablePaths() {
|
|
26150
|
+
return {
|
|
26151
|
+
relativeDirPath: ".",
|
|
26152
|
+
relativeFilePath: CRUSH_IGNORE_FILE_NAME
|
|
26153
|
+
};
|
|
26154
|
+
}
|
|
26155
|
+
toRulesyncIgnore() {
|
|
26156
|
+
return new RulesyncIgnore({
|
|
26157
|
+
outputRoot: ".",
|
|
26158
|
+
relativeDirPath: ".",
|
|
26159
|
+
relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
|
|
26160
|
+
fileContent: this.fileContent
|
|
26161
|
+
});
|
|
26162
|
+
}
|
|
26163
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
26164
|
+
const body = rulesyncIgnore.getFileContent();
|
|
26165
|
+
return new CrushIgnore({
|
|
26166
|
+
outputRoot,
|
|
26167
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26168
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26169
|
+
fileContent: body
|
|
26170
|
+
});
|
|
26171
|
+
}
|
|
26172
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
26173
|
+
const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
26174
|
+
return new CrushIgnore({
|
|
26175
|
+
outputRoot,
|
|
26176
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26177
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26178
|
+
fileContent,
|
|
26179
|
+
validate
|
|
26180
|
+
});
|
|
26181
|
+
}
|
|
26182
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
26183
|
+
return new CrushIgnore({
|
|
26184
|
+
outputRoot,
|
|
26185
|
+
relativeDirPath,
|
|
26186
|
+
relativeFilePath,
|
|
26187
|
+
fileContent: "",
|
|
26188
|
+
validate: false
|
|
26189
|
+
});
|
|
26190
|
+
}
|
|
26191
|
+
};
|
|
26192
|
+
//#endregion
|
|
25694
26193
|
//#region src/features/ignore/cursor-ignore.ts
|
|
25695
26194
|
/**
|
|
25696
26195
|
* Cursor ignore adapter.
|
|
@@ -26687,6 +27186,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
26687
27186
|
["claudecode", { class: ClaudecodeIgnore }],
|
|
26688
27187
|
["claudecode-legacy", { class: ClaudecodeIgnore }],
|
|
26689
27188
|
["cline", { class: ClineIgnore }],
|
|
27189
|
+
["crush", { class: CrushIgnore }],
|
|
26690
27190
|
["cursor", { class: CursorIgnore }],
|
|
26691
27191
|
["hermesagent", { class: HermesagentIgnore }],
|
|
26692
27192
|
["junie", { class: JunieIgnore }],
|
|
@@ -27991,9 +28491,8 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
27991
28491
|
throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
|
|
27992
28492
|
}
|
|
27993
28493
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
27994
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
27995
28494
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
27996
|
-
const rawServer =
|
|
28495
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
27997
28496
|
return [serverName, {
|
|
27998
28497
|
...serverConfig,
|
|
27999
28498
|
...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
@@ -30956,9 +31455,8 @@ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
|
|
|
30956
31455
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
30957
31456
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
30958
31457
|
const existing = parseMusecodeSettings(existingContent, filePath);
|
|
30959
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
30960
31458
|
const converted = convertToMusecodeFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
30961
|
-
const rawServer =
|
|
31459
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
30962
31460
|
const mode = asMusecodeMode(isRecord$1(rawServer) ? rawServer.musecodeMode : void 0);
|
|
30963
31461
|
return [serverName, {
|
|
30964
31462
|
...serverConfig,
|
|
@@ -31907,6 +32405,66 @@ async function readRovodevConfigYaml({ outputRoot }) {
|
|
|
31907
32405
|
filePath: join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
|
|
31908
32406
|
});
|
|
31909
32407
|
}
|
|
32408
|
+
/**
|
|
32409
|
+
* Decide the absolute path a configured `mcpConfigPath` would name in the
|
|
32410
|
+
* given scope, without yet checking whether that path stays inside it. Split
|
|
32411
|
+
* out of resolveRovodevMcpImportPath so each function's branching stays
|
|
32412
|
+
* within the project's complexity budget.
|
|
32413
|
+
*/
|
|
32414
|
+
function resolveMcpConfigCandidatePath({ outputRoot, global, normalizedPath, configuredPath }) {
|
|
32415
|
+
if (global && normalizedPath.startsWith("~/")) return { path: resolve(outputRoot, normalizedPath.slice(2)) };
|
|
32416
|
+
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 ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32417
|
+
if (isAbsolute(normalizedPath)) return { path: resolve(normalizedPath) };
|
|
32418
|
+
if (!global) return { path: resolve(outputRoot, normalizedPath) };
|
|
32419
|
+
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 ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32420
|
+
}
|
|
32421
|
+
/**
|
|
32422
|
+
* Resolve the active Rovo Dev MCP config without following a pointer outside
|
|
32423
|
+
* the import scope. The implementation is deliberately separate from
|
|
32424
|
+
* fromFile: it keeps path-policy decisions testable without changing the
|
|
32425
|
+
* public ToolMcp contract.
|
|
32426
|
+
*/
|
|
32427
|
+
async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger }) {
|
|
32428
|
+
const fallback = {
|
|
32429
|
+
filePath: join(outputRoot, ROVODEV_DIR, ROVODEV_MCP_FILE_NAME),
|
|
32430
|
+
relativeDirPath: ROVODEV_DIR,
|
|
32431
|
+
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32432
|
+
};
|
|
32433
|
+
const configuredPath = (config && isRecord$1(config.mcp) ? config.mcp : {}).mcpConfigPath;
|
|
32434
|
+
if (configuredPath === void 0) {
|
|
32435
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is unset in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)}, which may not be the file Rovo Dev reads.`);
|
|
32436
|
+
return fallback;
|
|
32437
|
+
}
|
|
32438
|
+
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
32439
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} must be a non-empty string. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32440
|
+
return fallback;
|
|
32441
|
+
}
|
|
32442
|
+
const normalizedPath = normalizeMcpConfigPathValue(configuredPath.trim());
|
|
32443
|
+
const candidateResult = resolveMcpConfigCandidatePath({
|
|
32444
|
+
outputRoot,
|
|
32445
|
+
global,
|
|
32446
|
+
normalizedPath,
|
|
32447
|
+
configuredPath
|
|
32448
|
+
});
|
|
32449
|
+
if ("rejectionMessage" in candidateResult) {
|
|
32450
|
+
logger?.warn(candidateResult.rejectionMessage);
|
|
32451
|
+
return fallback;
|
|
32452
|
+
}
|
|
32453
|
+
const candidatePath = candidateResult.path;
|
|
32454
|
+
const relativePath = relative(resolve(outputRoot), candidatePath);
|
|
32455
|
+
if (relativePath === "" || splitPathSegments(normalizedPath).includes("..") || pathEscapesRoot(relativePath) || await resolvedPathEscapesRoot({
|
|
32456
|
+
rootPath: outputRoot,
|
|
32457
|
+
targetPath: candidatePath
|
|
32458
|
+
})) {
|
|
32459
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)}, which is outside the import scope or traverses a symbolic link. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32460
|
+
return fallback;
|
|
32461
|
+
}
|
|
32462
|
+
return {
|
|
32463
|
+
filePath: candidatePath,
|
|
32464
|
+
relativeDirPath: dirname(relativePath),
|
|
32465
|
+
relativeFilePath: basename(relativePath)
|
|
32466
|
+
};
|
|
32467
|
+
}
|
|
31910
32468
|
function disabledNamesOf(config) {
|
|
31911
32469
|
const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
|
|
31912
32470
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
@@ -32012,6 +32570,25 @@ function envVarMcpFileSpellings({ fileName }) {
|
|
|
32012
32570
|
return [`$HOME/${tail}`, `\${HOME}/${tail}`];
|
|
32013
32571
|
}
|
|
32014
32572
|
/**
|
|
32573
|
+
* Classify the existing `mcpConfigPath` without deciding how to report it.
|
|
32574
|
+
* Keep the known-file checks in this order: the generated file is valid in
|
|
32575
|
+
* either scope, while the documented default and environment-variable
|
|
32576
|
+
* spellings are global-only alternatives that need their own warnings.
|
|
32577
|
+
*/
|
|
32578
|
+
function classifyExistingPointer({ existing, global, outputRoot }) {
|
|
32579
|
+
if (existing === void 0) return { kind: "unset" };
|
|
32580
|
+
const normalized = typeof existing === "string" ? normalizeMcpConfigPathValue(existing) : void 0;
|
|
32581
|
+
const namesFile = (fileName) => normalized !== void 0 && mcpFileSpellings({
|
|
32582
|
+
fileName,
|
|
32583
|
+
global,
|
|
32584
|
+
outputRoot
|
|
32585
|
+
}).includes(normalized);
|
|
32586
|
+
if (namesFile("mcp.json")) return { kind: "already-generated" };
|
|
32587
|
+
if (global && namesFile(ROVODEV_ALTERNATE_MCP_FILE_NAME)) return { kind: "documented-default" };
|
|
32588
|
+
if (global && normalized !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalized)) return { kind: "env-var-spelling" };
|
|
32589
|
+
return { kind: "unrelated" };
|
|
32590
|
+
}
|
|
32591
|
+
/**
|
|
32015
32592
|
* Point `mcp.mcpConfigPath` at the `mcp.json` rulesync writes for this scope,
|
|
32016
32593
|
* and report whether the block gained a value it did not already carry.
|
|
32017
32594
|
*
|
|
@@ -32122,45 +32699,44 @@ function announcePointer({ global, logger }) {
|
|
|
32122
32699
|
async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outputRoot, logger }) {
|
|
32123
32700
|
const { pointer, configLabel, mcpLabel } = pointerLabels(global);
|
|
32124
32701
|
const existing = existingMcp.mcpConfigPath;
|
|
32125
|
-
const
|
|
32126
|
-
|
|
32127
|
-
fileName,
|
|
32702
|
+
const classification = classifyExistingPointer({
|
|
32703
|
+
existing,
|
|
32128
32704
|
global,
|
|
32129
32705
|
outputRoot
|
|
32130
|
-
})
|
|
32131
|
-
const pointsAtGeneratedFile = namesFile(ROVODEV_MCP_FILE_NAME);
|
|
32706
|
+
});
|
|
32132
32707
|
if (!hasLiveServers) {
|
|
32133
|
-
if (
|
|
32708
|
+
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"}.`);
|
|
32134
32709
|
return false;
|
|
32135
32710
|
}
|
|
32136
|
-
|
|
32137
|
-
|
|
32138
|
-
|
|
32139
|
-
|
|
32140
|
-
|
|
32711
|
+
switch (classification.kind) {
|
|
32712
|
+
case "unset": {
|
|
32713
|
+
const displaced = global ? await describeDisplacedGlobalServers({ outputRoot }) : null;
|
|
32714
|
+
if (displaced !== null) {
|
|
32715
|
+
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.`);
|
|
32716
|
+
return false;
|
|
32717
|
+
}
|
|
32718
|
+
existingMcp.mcpConfigPath = pointer;
|
|
32719
|
+
announcePointer({
|
|
32720
|
+
global,
|
|
32721
|
+
logger
|
|
32722
|
+
});
|
|
32723
|
+
return true;
|
|
32141
32724
|
}
|
|
32142
|
-
|
|
32143
|
-
|
|
32144
|
-
|
|
32145
|
-
|
|
32146
|
-
|
|
32147
|
-
|
|
32148
|
-
|
|
32149
|
-
|
|
32150
|
-
|
|
32151
|
-
|
|
32152
|
-
|
|
32153
|
-
|
|
32154
|
-
logger
|
|
32155
|
-
|
|
32156
|
-
return false;
|
|
32157
|
-
}
|
|
32158
|
-
if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
|
|
32159
|
-
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.`);
|
|
32160
|
-
return false;
|
|
32725
|
+
case "already-generated": return false;
|
|
32726
|
+
case "documented-default":
|
|
32727
|
+
await warnAtDocumentedDefault({
|
|
32728
|
+
existing,
|
|
32729
|
+
outputRoot,
|
|
32730
|
+
logger
|
|
32731
|
+
});
|
|
32732
|
+
return false;
|
|
32733
|
+
case "env-var-spelling":
|
|
32734
|
+
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.`);
|
|
32735
|
+
return false;
|
|
32736
|
+
case "unrelated":
|
|
32737
|
+
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}".`);
|
|
32738
|
+
return false;
|
|
32161
32739
|
}
|
|
32162
|
-
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}".`);
|
|
32163
|
-
return false;
|
|
32164
32740
|
}
|
|
32165
32741
|
/**
|
|
32166
32742
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
@@ -32202,14 +32778,20 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32202
32778
|
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32203
32779
|
};
|
|
32204
32780
|
}
|
|
32205
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32206
|
-
const
|
|
32207
|
-
const
|
|
32781
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
32782
|
+
const rovodevConfig = await readRovodevConfigYaml({ outputRoot });
|
|
32783
|
+
const paths = await resolveRovodevMcpImportPath({
|
|
32784
|
+
outputRoot,
|
|
32785
|
+
global,
|
|
32786
|
+
config: rovodevConfig,
|
|
32787
|
+
logger
|
|
32788
|
+
});
|
|
32789
|
+
const json = parseRovodevMcpJson(await readFileContentOrNull(paths.filePath) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
|
|
32208
32790
|
const newJson = {
|
|
32209
32791
|
...json,
|
|
32210
32792
|
mcpServers: json.mcpServers ?? {}
|
|
32211
32793
|
};
|
|
32212
|
-
const disabledNames = disabledNamesOf(
|
|
32794
|
+
const disabledNames = disabledNamesOf(rovodevConfig);
|
|
32213
32795
|
if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
|
|
32214
32796
|
const servers = newJson.mcpServers;
|
|
32215
32797
|
for (const name of disabledNames) {
|
|
@@ -32239,9 +32821,8 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32239
32821
|
} catch {
|
|
32240
32822
|
canWriteDisableToggle = false;
|
|
32241
32823
|
}
|
|
32242
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
32243
32824
|
const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
|
|
32244
|
-
const rawServer =
|
|
32825
|
+
const rawServer = rulesyncMcp.getRawMcpServer(name);
|
|
32245
32826
|
const record = {
|
|
32246
32827
|
...server,
|
|
32247
32828
|
...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
|
|
@@ -34229,6 +34810,518 @@ function convertAmpToRulesync({ disable, permissions }) {
|
|
|
34229
34810
|
return { permission };
|
|
34230
34811
|
}
|
|
34231
34812
|
//#endregion
|
|
34813
|
+
//#region src/utils/glob.ts
|
|
34814
|
+
/**
|
|
34815
|
+
* Convert a glob-like pattern into an anchored regex source string.
|
|
34816
|
+
*
|
|
34817
|
+
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34818
|
+
* every other regex metacharacter is escaped so it matches literally. The
|
|
34819
|
+
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34820
|
+
* name?" rather than "does this appear somewhere in it?".
|
|
34821
|
+
*
|
|
34822
|
+
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34823
|
+
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34824
|
+
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34825
|
+
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34826
|
+
* has to say what the tool would read rather than what a glob means. Use
|
|
34827
|
+
* `matchesGlob` for an actual comparison.
|
|
34828
|
+
*/
|
|
34829
|
+
function globToAnchoredRegexSource(glob) {
|
|
34830
|
+
let source = "";
|
|
34831
|
+
for (const char of glob) if (char === "*") source += ".*";
|
|
34832
|
+
else if (char === "?") source += ".";
|
|
34833
|
+
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34834
|
+
else source += char;
|
|
34835
|
+
return `^${source}$`;
|
|
34836
|
+
}
|
|
34837
|
+
/**
|
|
34838
|
+
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34839
|
+
* bracket is never closed — in which case it is an ordinary character.
|
|
34840
|
+
*/
|
|
34841
|
+
function parseGlobClass(characters, start) {
|
|
34842
|
+
let index = start;
|
|
34843
|
+
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34844
|
+
if (negated) index += 1;
|
|
34845
|
+
const members = /* @__PURE__ */ new Set();
|
|
34846
|
+
const ranges = [];
|
|
34847
|
+
let first = true;
|
|
34848
|
+
while (index < characters.length) {
|
|
34849
|
+
const character = characters[index] ?? "";
|
|
34850
|
+
if (character === "]" && !first) return {
|
|
34851
|
+
step: {
|
|
34852
|
+
kind: "class",
|
|
34853
|
+
negated,
|
|
34854
|
+
members,
|
|
34855
|
+
ranges
|
|
34856
|
+
},
|
|
34857
|
+
next: index + 1
|
|
34858
|
+
};
|
|
34859
|
+
first = false;
|
|
34860
|
+
const high = characters[index + 2];
|
|
34861
|
+
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34862
|
+
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34863
|
+
index += 3;
|
|
34864
|
+
continue;
|
|
34865
|
+
}
|
|
34866
|
+
members.add(character);
|
|
34867
|
+
index += 1;
|
|
34868
|
+
}
|
|
34869
|
+
}
|
|
34870
|
+
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34871
|
+
function parseGlob(glob) {
|
|
34872
|
+
const characters = [...glob];
|
|
34873
|
+
const steps = [];
|
|
34874
|
+
let index = 0;
|
|
34875
|
+
let bracketsAreClosed = true;
|
|
34876
|
+
while (index < characters.length) {
|
|
34877
|
+
const character = characters[index] ?? "";
|
|
34878
|
+
index += 1;
|
|
34879
|
+
if (character === "*") {
|
|
34880
|
+
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34881
|
+
continue;
|
|
34882
|
+
}
|
|
34883
|
+
if (character === "?") {
|
|
34884
|
+
steps.push({ kind: "any" });
|
|
34885
|
+
continue;
|
|
34886
|
+
}
|
|
34887
|
+
if (character === "[" && bracketsAreClosed) {
|
|
34888
|
+
const parsed = parseGlobClass(characters, index);
|
|
34889
|
+
if (parsed === void 0) bracketsAreClosed = false;
|
|
34890
|
+
else {
|
|
34891
|
+
steps.push(parsed.step);
|
|
34892
|
+
index = parsed.next;
|
|
34893
|
+
continue;
|
|
34894
|
+
}
|
|
34895
|
+
}
|
|
34896
|
+
steps.push({
|
|
34897
|
+
kind: "literal",
|
|
34898
|
+
character
|
|
34899
|
+
});
|
|
34900
|
+
}
|
|
34901
|
+
return steps;
|
|
34902
|
+
}
|
|
34903
|
+
function matchesGlobStep(step, character) {
|
|
34904
|
+
if (step.kind === "star") return false;
|
|
34905
|
+
if (step.kind === "any") return true;
|
|
34906
|
+
if (step.kind === "literal") return step.character === character;
|
|
34907
|
+
const code = character.codePointAt(0) ?? 0;
|
|
34908
|
+
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34909
|
+
return step.negated ? !admitted : admitted;
|
|
34910
|
+
}
|
|
34911
|
+
/** Whether two single-character steps can both match one same character. */
|
|
34912
|
+
function stepsShareACharacter(left, right) {
|
|
34913
|
+
if (left.kind === "any" || right.kind === "any") return true;
|
|
34914
|
+
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34915
|
+
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34916
|
+
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34917
|
+
return true;
|
|
34918
|
+
}
|
|
34919
|
+
/** Whether every step from `index` on can match the empty string. */
|
|
34920
|
+
function isAllStars(steps, index) {
|
|
34921
|
+
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34922
|
+
return true;
|
|
34923
|
+
}
|
|
34924
|
+
/**
|
|
34925
|
+
* The most work one intersection walk will do, counted in cells times the cost
|
|
34926
|
+
* of one. Past it the two patterns are reported as intersecting without being
|
|
34927
|
+
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34928
|
+
* enough to reach this is pathological rather than a command anybody typed.
|
|
34929
|
+
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34930
|
+
* closed.
|
|
34931
|
+
*/
|
|
34932
|
+
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34933
|
+
/**
|
|
34934
|
+
* The most work a whole run of comparisons will do. A caller holding R
|
|
34935
|
+
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34936
|
+
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34937
|
+
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34938
|
+
* that together take minutes. The shared budget is spent down across the run
|
|
34939
|
+
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34940
|
+
* again the direction that withholds an `allow` rather than writing one.
|
|
34941
|
+
*/
|
|
34942
|
+
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34943
|
+
/**
|
|
34944
|
+
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34945
|
+
* filling the two rows the table is held in, and collecting the answer.
|
|
34946
|
+
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34947
|
+
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34948
|
+
* short allow rules is n squared comparisons that never spend the budget down
|
|
34949
|
+
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34950
|
+
* walk length on the same exhaustible resource.
|
|
34951
|
+
*
|
|
34952
|
+
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34953
|
+
* which lowers how many pairs a run compares from around a million to about
|
|
34954
|
+
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34955
|
+
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34956
|
+
* answer exhaustion gives everywhere else.
|
|
34957
|
+
*/
|
|
34958
|
+
const INTERSECTION_PAIR_COST = 64;
|
|
34959
|
+
/**
|
|
34960
|
+
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34961
|
+
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34962
|
+
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34963
|
+
* it.
|
|
34964
|
+
*/
|
|
34965
|
+
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34966
|
+
return { remaining };
|
|
34967
|
+
}
|
|
34968
|
+
/**
|
|
34969
|
+
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34970
|
+
* the same pattern against a whole list parses it once and reuses the result.
|
|
34971
|
+
*/
|
|
34972
|
+
function parseGlobPattern(glob) {
|
|
34973
|
+
const steps = parseGlob(glob);
|
|
34974
|
+
return {
|
|
34975
|
+
steps,
|
|
34976
|
+
maxRanges: maxRangeCount(steps)
|
|
34977
|
+
};
|
|
34978
|
+
}
|
|
34979
|
+
/**
|
|
34980
|
+
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34981
|
+
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34982
|
+
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34983
|
+
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34984
|
+
* than on cells.
|
|
34985
|
+
*/
|
|
34986
|
+
function maxRangeCount(steps) {
|
|
34987
|
+
let most = 0;
|
|
34988
|
+
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34989
|
+
return most;
|
|
34990
|
+
}
|
|
34991
|
+
/**
|
|
34992
|
+
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34993
|
+
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34994
|
+
* Once that budget is exhausted every further pair answers `true` without being
|
|
34995
|
+
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34996
|
+
* safe side.
|
|
34997
|
+
*/
|
|
34998
|
+
function parsedGlobsIntersect(left, right, budget) {
|
|
34999
|
+
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
35000
|
+
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
35001
|
+
const cost = rows.length * columns.length * cellCost;
|
|
35002
|
+
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
35003
|
+
if (budget !== void 0) {
|
|
35004
|
+
const charge = cost + INTERSECTION_PAIR_COST;
|
|
35005
|
+
if (charge > budget.remaining) {
|
|
35006
|
+
budget.remaining = 0;
|
|
35007
|
+
return true;
|
|
35008
|
+
}
|
|
35009
|
+
budget.remaining -= charge;
|
|
35010
|
+
}
|
|
35011
|
+
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
35012
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
35013
|
+
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
35014
|
+
row[columns.length] = isAllStars(rows, i);
|
|
35015
|
+
for (let j = columns.length - 1; j >= 0; j--) {
|
|
35016
|
+
const rowStep = rows[i];
|
|
35017
|
+
const columnStep = columns[j];
|
|
35018
|
+
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
35019
|
+
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
35020
|
+
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
35021
|
+
continue;
|
|
35022
|
+
}
|
|
35023
|
+
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
35024
|
+
}
|
|
35025
|
+
next = row;
|
|
35026
|
+
}
|
|
35027
|
+
return next[0] ?? false;
|
|
35028
|
+
}
|
|
35029
|
+
//#endregion
|
|
35030
|
+
//#region src/features/permissions/shell-command-categories.ts
|
|
35031
|
+
/** The canonical category that names a shell command's permissions. */
|
|
35032
|
+
const SHELL_PERMISSION_CATEGORY = "bash";
|
|
35033
|
+
/**
|
|
35034
|
+
* Collect the canonical rules that govern shell commands, for the adapters
|
|
35035
|
+
* whose tool models commands and nothing else.
|
|
35036
|
+
*
|
|
35037
|
+
* The `bash` category contributes every rule. The all-tools `*` category
|
|
35038
|
+
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
35039
|
+
* written there covers shell commands too, and dropping it inverts the
|
|
35040
|
+
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
35041
|
+
* an adapter that reads only `bash` auto-approves the very command the file
|
|
35042
|
+
* denies.
|
|
35043
|
+
*
|
|
35044
|
+
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35045
|
+
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35046
|
+
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35047
|
+
* it in the permissive direction would grant something the author never said
|
|
35048
|
+
* about commands. Both directions therefore fail closed.
|
|
35049
|
+
*/
|
|
35050
|
+
function collectShellCommandRules(permission) {
|
|
35051
|
+
const rules = [];
|
|
35052
|
+
const foreignRestrictingCategories = [];
|
|
35053
|
+
const ignoredAllToolsAllowPatterns = [];
|
|
35054
|
+
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35055
|
+
if (category === "bash") {
|
|
35056
|
+
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35057
|
+
pattern,
|
|
35058
|
+
action,
|
|
35059
|
+
fromAllToolsCategory: false
|
|
35060
|
+
});
|
|
35061
|
+
continue;
|
|
35062
|
+
}
|
|
35063
|
+
if (category === "*") {
|
|
35064
|
+
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35065
|
+
if (action === "allow") {
|
|
35066
|
+
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35067
|
+
continue;
|
|
35068
|
+
}
|
|
35069
|
+
rules.push({
|
|
35070
|
+
pattern,
|
|
35071
|
+
action,
|
|
35072
|
+
fromAllToolsCategory: true
|
|
35073
|
+
});
|
|
35074
|
+
}
|
|
35075
|
+
continue;
|
|
35076
|
+
}
|
|
35077
|
+
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35078
|
+
}
|
|
35079
|
+
return {
|
|
35080
|
+
rules,
|
|
35081
|
+
foreignRestrictingCategories,
|
|
35082
|
+
ignoredAllToolsAllowPatterns
|
|
35083
|
+
};
|
|
35084
|
+
}
|
|
35085
|
+
/**
|
|
35086
|
+
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35087
|
+
* which restrictions it cannot write name some of the same commands? The
|
|
35088
|
+
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35089
|
+
* written — so a caller can report both the allow rules it withheld and the
|
|
35090
|
+
* restrictions that withheld nothing.
|
|
35091
|
+
*
|
|
35092
|
+
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35093
|
+
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35094
|
+
* asking whether any one command matches both. Width does not enter into it: an
|
|
35095
|
+
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35096
|
+
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35097
|
+
* on every `git ... --force` command even though neither pattern covers the
|
|
35098
|
+
* other's spelling. Comparing only identical spellings would let the most
|
|
35099
|
+
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35100
|
+
*
|
|
35101
|
+
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35102
|
+
* the walk for the commonest case.
|
|
35103
|
+
*
|
|
35104
|
+
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35105
|
+
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35106
|
+
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35107
|
+
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35108
|
+
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35109
|
+
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35110
|
+
* withholds an allow rather than writing one the config restricts — see
|
|
35111
|
+
* `warpCommandPatternToGlob`.
|
|
35112
|
+
*/
|
|
35113
|
+
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35114
|
+
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35115
|
+
pattern,
|
|
35116
|
+
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35117
|
+
}));
|
|
35118
|
+
return (allowPattern) => {
|
|
35119
|
+
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35120
|
+
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35121
|
+
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35122
|
+
};
|
|
35123
|
+
}
|
|
35124
|
+
/**
|
|
35125
|
+
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35126
|
+
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35127
|
+
*
|
|
35128
|
+
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35129
|
+
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35130
|
+
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35131
|
+
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35132
|
+
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35133
|
+
*
|
|
35134
|
+
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35135
|
+
* so overlapping no allow rule says nothing is wrong with it.
|
|
35136
|
+
*/
|
|
35137
|
+
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35138
|
+
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35139
|
+
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35140
|
+
return uniq(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35141
|
+
}
|
|
35142
|
+
/**
|
|
35143
|
+
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35144
|
+
* commands with those two tiers and nothing else.
|
|
35145
|
+
*
|
|
35146
|
+
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35147
|
+
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35148
|
+
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35149
|
+
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35150
|
+
* about would answer the prompt the author wanted.
|
|
35151
|
+
*
|
|
35152
|
+
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35153
|
+
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35154
|
+
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35155
|
+
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35156
|
+
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35157
|
+
* cannot be written it withholds the allow rules it covers instead, which
|
|
35158
|
+
* restricts in the same direction without touching the denylist.
|
|
35159
|
+
*
|
|
35160
|
+
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35161
|
+
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35162
|
+
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35163
|
+
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35164
|
+
* even where it is written: a pattern under `*` need not name a command —
|
|
35165
|
+
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35166
|
+
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35167
|
+
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35168
|
+
* command pattern is reported; failing open would not be.
|
|
35169
|
+
*
|
|
35170
|
+
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35171
|
+
* patterns are not globs.
|
|
35172
|
+
*/
|
|
35173
|
+
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35174
|
+
const deny = [];
|
|
35175
|
+
const unwrittenDenyPatterns = [];
|
|
35176
|
+
const restrictions = [];
|
|
35177
|
+
const writtenAllToolsDenyPatterns = [];
|
|
35178
|
+
const allToolsAskPatterns = [];
|
|
35179
|
+
for (const rule of rules) {
|
|
35180
|
+
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35181
|
+
if (action === "allow") continue;
|
|
35182
|
+
if (action !== "deny") {
|
|
35183
|
+
restrictions.push(rule);
|
|
35184
|
+
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35185
|
+
continue;
|
|
35186
|
+
}
|
|
35187
|
+
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35188
|
+
deny.push(pattern);
|
|
35189
|
+
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35190
|
+
} else unwrittenDenyPatterns.push(pattern);
|
|
35191
|
+
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35192
|
+
}
|
|
35193
|
+
const budget = createIntersectionBudget();
|
|
35194
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35195
|
+
normalizePattern,
|
|
35196
|
+
budget
|
|
35197
|
+
});
|
|
35198
|
+
const allow = [];
|
|
35199
|
+
const shadowedAllowPatterns = [];
|
|
35200
|
+
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35201
|
+
for (const { pattern, action } of rules) {
|
|
35202
|
+
if (action !== "allow") continue;
|
|
35203
|
+
const shadowing = shadowingRestrictions(pattern);
|
|
35204
|
+
if (shadowing.length > 0) {
|
|
35205
|
+
shadowedAllowPatterns.push(pattern);
|
|
35206
|
+
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35207
|
+
continue;
|
|
35208
|
+
}
|
|
35209
|
+
allow.push(pattern);
|
|
35210
|
+
}
|
|
35211
|
+
return {
|
|
35212
|
+
allow,
|
|
35213
|
+
deny,
|
|
35214
|
+
shadowedAllowPatterns,
|
|
35215
|
+
unwrittenDenyPatterns,
|
|
35216
|
+
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35217
|
+
rules,
|
|
35218
|
+
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35219
|
+
withholdingPatterns
|
|
35220
|
+
}),
|
|
35221
|
+
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35222
|
+
rules,
|
|
35223
|
+
allToolsPatterns: allToolsAskPatterns,
|
|
35224
|
+
withholdingPatterns
|
|
35225
|
+
}),
|
|
35226
|
+
intersectionBudgetExhausted: budget.remaining === 0
|
|
35227
|
+
};
|
|
35228
|
+
}
|
|
35229
|
+
/**
|
|
35230
|
+
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35231
|
+
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35232
|
+
* dropped in one is worded the same way in all.
|
|
35233
|
+
*/
|
|
35234
|
+
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35235
|
+
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.`);
|
|
35236
|
+
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.`);
|
|
35237
|
+
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.`);
|
|
35238
|
+
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.`);
|
|
35239
|
+
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.`);
|
|
35240
|
+
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.`);
|
|
35241
|
+
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.`);
|
|
35242
|
+
}
|
|
35243
|
+
function resolveShellCommandState(permission, writesAllToolsDeny) {
|
|
35244
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permission);
|
|
35245
|
+
const partitioned = partitionCommandRules({
|
|
35246
|
+
rules,
|
|
35247
|
+
writesAllToolsDeny
|
|
35248
|
+
});
|
|
35249
|
+
return {
|
|
35250
|
+
allow: partitioned.allow,
|
|
35251
|
+
deny: partitioned.deny,
|
|
35252
|
+
bash: bashRulesHonoringAllTools(permission),
|
|
35253
|
+
foreignRestrictingCategories,
|
|
35254
|
+
ignoredAllToolsAllowPatterns,
|
|
35255
|
+
shadowedAllowPatterns: partitioned.shadowedAllowPatterns,
|
|
35256
|
+
unwrittenDenyPatterns: partitioned.unwrittenDenyPatterns,
|
|
35257
|
+
unenforcedAllToolsDenyPatterns: partitioned.unenforcedAllToolsDenyPatterns,
|
|
35258
|
+
unenforcedAllToolsAskPatterns: partitioned.unenforcedAllToolsAskPatterns,
|
|
35259
|
+
intersectionBudgetExhausted: partitioned.intersectionBudgetExhausted
|
|
35260
|
+
};
|
|
35261
|
+
}
|
|
35262
|
+
/**
|
|
35263
|
+
* Collect shell-command allow/deny lists the way the command-only adapters do,
|
|
35264
|
+
* and report every restriction the surface cannot carry.
|
|
35265
|
+
*/
|
|
35266
|
+
function resolveShellCommandLists({ permission, writesAllToolsDeny, toolLabel, surfaceLabel, logger }) {
|
|
35267
|
+
const resolved = resolveShellCommandState(permission, writesAllToolsDeny);
|
|
35268
|
+
warnAboutUnwrittenCommandRules({
|
|
35269
|
+
toolLabel,
|
|
35270
|
+
surfaceLabel,
|
|
35271
|
+
foreignRestrictingCategories: resolved.foreignRestrictingCategories,
|
|
35272
|
+
shadowedAllowPatterns: resolved.shadowedAllowPatterns,
|
|
35273
|
+
unwrittenDenyPatterns: resolved.unwrittenDenyPatterns,
|
|
35274
|
+
unenforcedAllToolsDenyPatterns: resolved.unenforcedAllToolsDenyPatterns,
|
|
35275
|
+
unenforcedAllToolsAskPatterns: resolved.unenforcedAllToolsAskPatterns,
|
|
35276
|
+
ignoredAllToolsAllowPatterns: resolved.ignoredAllToolsAllowPatterns,
|
|
35277
|
+
intersectionBudgetExhausted: resolved.intersectionBudgetExhausted,
|
|
35278
|
+
logger
|
|
35279
|
+
});
|
|
35280
|
+
return {
|
|
35281
|
+
allow: resolved.allow,
|
|
35282
|
+
deny: resolved.deny,
|
|
35283
|
+
bash: resolved.bash
|
|
35284
|
+
};
|
|
35285
|
+
}
|
|
35286
|
+
/**
|
|
35287
|
+
* The `bash` category after all-tools `*` restrictions have been applied. A
|
|
35288
|
+
* `deny`/`ask` written under `*` covers shell commands too, so a bash `allow`
|
|
35289
|
+
* it overlaps is withheld, a `*` deny is copied in, and a `*` ask is copied in
|
|
35290
|
+
* wherever `bash` says nothing about that exact pattern yet — otherwise it
|
|
35291
|
+
* would vanish from the resolved category entirely rather than falling back to
|
|
35292
|
+
* a tier that still prompts. An existing `bash` entry for the same pattern is
|
|
35293
|
+
* never downgraded by a `*` ask (a bash `allow` was already dropped above, and
|
|
35294
|
+
* a bash `deny`/`ask` there is at least as strict already).
|
|
35295
|
+
*/
|
|
35296
|
+
function bashRulesHonoringAllTools(permission) {
|
|
35297
|
+
const { rules } = collectShellCommandRules(permission);
|
|
35298
|
+
const allToolsRestrictions = rules.filter(({ fromAllToolsCategory }) => fromAllToolsCategory);
|
|
35299
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(allToolsRestrictions);
|
|
35300
|
+
const bash = { ...permission.bash };
|
|
35301
|
+
for (const [pattern, action] of Object.entries(bash)) if (action === "allow" && shadowingRestrictions(pattern).length > 0) delete bash[pattern];
|
|
35302
|
+
for (const { pattern, action } of allToolsRestrictions) {
|
|
35303
|
+
if (isPrototypePollutionKey(pattern)) continue;
|
|
35304
|
+
if (action === "deny") {
|
|
35305
|
+
if (bash[pattern] !== "ask") bash[pattern] = "deny";
|
|
35306
|
+
continue;
|
|
35307
|
+
}
|
|
35308
|
+
if (bash[pattern] === void 0) bash[pattern] = "ask";
|
|
35309
|
+
}
|
|
35310
|
+
return bash;
|
|
35311
|
+
}
|
|
35312
|
+
/**
|
|
35313
|
+
* Return a permission block whose `bash` category honors all-tools `*`
|
|
35314
|
+
* restrictions. Other categories are unchanged, so adapters that already model
|
|
35315
|
+
* `*` keep doing so.
|
|
35316
|
+
*/
|
|
35317
|
+
function honorAllToolsOnBash(permission) {
|
|
35318
|
+
if (permission.bash === void 0) return permission;
|
|
35319
|
+
return {
|
|
35320
|
+
...permission,
|
|
35321
|
+
bash: bashRulesHonoringAllTools(permission)
|
|
35322
|
+
};
|
|
35323
|
+
}
|
|
35324
|
+
//#endregion
|
|
34232
35325
|
//#region src/features/permissions/antigravity-cli-permissions.ts
|
|
34233
35326
|
/**
|
|
34234
35327
|
* Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
|
|
@@ -34476,7 +35569,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
|
|
|
34476
35569
|
const allow = [];
|
|
34477
35570
|
const ask = [];
|
|
34478
35571
|
const deny = [];
|
|
34479
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35572
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34480
35573
|
const cliToolName = toAntigravityCliToolName(category);
|
|
34481
35574
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
34482
35575
|
const entry = buildPermissionEntry$1(cliToolName, pattern);
|
|
@@ -34687,7 +35780,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
|
|
|
34687
35780
|
const allow = [];
|
|
34688
35781
|
const ask = [];
|
|
34689
35782
|
const deny = [];
|
|
34690
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35783
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34691
35784
|
const action = toIdeAction(category);
|
|
34692
35785
|
for (const [pattern, permissionAction] of Object.entries(rules)) {
|
|
34693
35786
|
const entry = buildPermissionEntry(action, pattern);
|
|
@@ -34727,223 +35820,6 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
34727
35820
|
return { permission };
|
|
34728
35821
|
}
|
|
34729
35822
|
//#endregion
|
|
34730
|
-
//#region src/utils/glob.ts
|
|
34731
|
-
/**
|
|
34732
|
-
* Convert a glob-like pattern into an anchored regex source string.
|
|
34733
|
-
*
|
|
34734
|
-
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34735
|
-
* every other regex metacharacter is escaped so it matches literally. The
|
|
34736
|
-
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34737
|
-
* name?" rather than "does this appear somewhere in it?".
|
|
34738
|
-
*
|
|
34739
|
-
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34740
|
-
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34741
|
-
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34742
|
-
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34743
|
-
* has to say what the tool would read rather than what a glob means. Use
|
|
34744
|
-
* `matchesGlob` for an actual comparison.
|
|
34745
|
-
*/
|
|
34746
|
-
function globToAnchoredRegexSource(glob) {
|
|
34747
|
-
let source = "";
|
|
34748
|
-
for (const char of glob) if (char === "*") source += ".*";
|
|
34749
|
-
else if (char === "?") source += ".";
|
|
34750
|
-
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34751
|
-
else source += char;
|
|
34752
|
-
return `^${source}$`;
|
|
34753
|
-
}
|
|
34754
|
-
/**
|
|
34755
|
-
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34756
|
-
* bracket is never closed — in which case it is an ordinary character.
|
|
34757
|
-
*/
|
|
34758
|
-
function parseGlobClass(characters, start) {
|
|
34759
|
-
let index = start;
|
|
34760
|
-
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34761
|
-
if (negated) index += 1;
|
|
34762
|
-
const members = /* @__PURE__ */ new Set();
|
|
34763
|
-
const ranges = [];
|
|
34764
|
-
let first = true;
|
|
34765
|
-
while (index < characters.length) {
|
|
34766
|
-
const character = characters[index] ?? "";
|
|
34767
|
-
if (character === "]" && !first) return {
|
|
34768
|
-
step: {
|
|
34769
|
-
kind: "class",
|
|
34770
|
-
negated,
|
|
34771
|
-
members,
|
|
34772
|
-
ranges
|
|
34773
|
-
},
|
|
34774
|
-
next: index + 1
|
|
34775
|
-
};
|
|
34776
|
-
first = false;
|
|
34777
|
-
const high = characters[index + 2];
|
|
34778
|
-
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34779
|
-
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34780
|
-
index += 3;
|
|
34781
|
-
continue;
|
|
34782
|
-
}
|
|
34783
|
-
members.add(character);
|
|
34784
|
-
index += 1;
|
|
34785
|
-
}
|
|
34786
|
-
}
|
|
34787
|
-
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34788
|
-
function parseGlob(glob) {
|
|
34789
|
-
const characters = [...glob];
|
|
34790
|
-
const steps = [];
|
|
34791
|
-
let index = 0;
|
|
34792
|
-
let bracketsAreClosed = true;
|
|
34793
|
-
while (index < characters.length) {
|
|
34794
|
-
const character = characters[index] ?? "";
|
|
34795
|
-
index += 1;
|
|
34796
|
-
if (character === "*") {
|
|
34797
|
-
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34798
|
-
continue;
|
|
34799
|
-
}
|
|
34800
|
-
if (character === "?") {
|
|
34801
|
-
steps.push({ kind: "any" });
|
|
34802
|
-
continue;
|
|
34803
|
-
}
|
|
34804
|
-
if (character === "[" && bracketsAreClosed) {
|
|
34805
|
-
const parsed = parseGlobClass(characters, index);
|
|
34806
|
-
if (parsed === void 0) bracketsAreClosed = false;
|
|
34807
|
-
else {
|
|
34808
|
-
steps.push(parsed.step);
|
|
34809
|
-
index = parsed.next;
|
|
34810
|
-
continue;
|
|
34811
|
-
}
|
|
34812
|
-
}
|
|
34813
|
-
steps.push({
|
|
34814
|
-
kind: "literal",
|
|
34815
|
-
character
|
|
34816
|
-
});
|
|
34817
|
-
}
|
|
34818
|
-
return steps;
|
|
34819
|
-
}
|
|
34820
|
-
function matchesGlobStep(step, character) {
|
|
34821
|
-
if (step.kind === "star") return false;
|
|
34822
|
-
if (step.kind === "any") return true;
|
|
34823
|
-
if (step.kind === "literal") return step.character === character;
|
|
34824
|
-
const code = character.codePointAt(0) ?? 0;
|
|
34825
|
-
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34826
|
-
return step.negated ? !admitted : admitted;
|
|
34827
|
-
}
|
|
34828
|
-
/** Whether two single-character steps can both match one same character. */
|
|
34829
|
-
function stepsShareACharacter(left, right) {
|
|
34830
|
-
if (left.kind === "any" || right.kind === "any") return true;
|
|
34831
|
-
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34832
|
-
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34833
|
-
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34834
|
-
return true;
|
|
34835
|
-
}
|
|
34836
|
-
/** Whether every step from `index` on can match the empty string. */
|
|
34837
|
-
function isAllStars(steps, index) {
|
|
34838
|
-
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34839
|
-
return true;
|
|
34840
|
-
}
|
|
34841
|
-
/**
|
|
34842
|
-
* The most work one intersection walk will do, counted in cells times the cost
|
|
34843
|
-
* of one. Past it the two patterns are reported as intersecting without being
|
|
34844
|
-
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34845
|
-
* enough to reach this is pathological rather than a command anybody typed.
|
|
34846
|
-
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34847
|
-
* closed.
|
|
34848
|
-
*/
|
|
34849
|
-
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34850
|
-
/**
|
|
34851
|
-
* The most work a whole run of comparisons will do. A caller holding R
|
|
34852
|
-
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34853
|
-
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34854
|
-
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34855
|
-
* that together take minutes. The shared budget is spent down across the run
|
|
34856
|
-
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34857
|
-
* again the direction that withholds an `allow` rather than writing one.
|
|
34858
|
-
*/
|
|
34859
|
-
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34860
|
-
/**
|
|
34861
|
-
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34862
|
-
* filling the two rows the table is held in, and collecting the answer.
|
|
34863
|
-
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34864
|
-
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34865
|
-
* short allow rules is n squared comparisons that never spend the budget down
|
|
34866
|
-
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34867
|
-
* walk length on the same exhaustible resource.
|
|
34868
|
-
*
|
|
34869
|
-
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34870
|
-
* which lowers how many pairs a run compares from around a million to about
|
|
34871
|
-
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34872
|
-
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34873
|
-
* answer exhaustion gives everywhere else.
|
|
34874
|
-
*/
|
|
34875
|
-
const INTERSECTION_PAIR_COST = 64;
|
|
34876
|
-
/**
|
|
34877
|
-
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34878
|
-
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34879
|
-
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34880
|
-
* it.
|
|
34881
|
-
*/
|
|
34882
|
-
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34883
|
-
return { remaining };
|
|
34884
|
-
}
|
|
34885
|
-
/**
|
|
34886
|
-
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34887
|
-
* the same pattern against a whole list parses it once and reuses the result.
|
|
34888
|
-
*/
|
|
34889
|
-
function parseGlobPattern(glob) {
|
|
34890
|
-
const steps = parseGlob(glob);
|
|
34891
|
-
return {
|
|
34892
|
-
steps,
|
|
34893
|
-
maxRanges: maxRangeCount(steps)
|
|
34894
|
-
};
|
|
34895
|
-
}
|
|
34896
|
-
/**
|
|
34897
|
-
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34898
|
-
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34899
|
-
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34900
|
-
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34901
|
-
* than on cells.
|
|
34902
|
-
*/
|
|
34903
|
-
function maxRangeCount(steps) {
|
|
34904
|
-
let most = 0;
|
|
34905
|
-
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34906
|
-
return most;
|
|
34907
|
-
}
|
|
34908
|
-
/**
|
|
34909
|
-
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34910
|
-
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34911
|
-
* Once that budget is exhausted every further pair answers `true` without being
|
|
34912
|
-
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34913
|
-
* safe side.
|
|
34914
|
-
*/
|
|
34915
|
-
function parsedGlobsIntersect(left, right, budget) {
|
|
34916
|
-
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
34917
|
-
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
34918
|
-
const cost = rows.length * columns.length * cellCost;
|
|
34919
|
-
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
34920
|
-
if (budget !== void 0) {
|
|
34921
|
-
const charge = cost + INTERSECTION_PAIR_COST;
|
|
34922
|
-
if (charge > budget.remaining) {
|
|
34923
|
-
budget.remaining = 0;
|
|
34924
|
-
return true;
|
|
34925
|
-
}
|
|
34926
|
-
budget.remaining -= charge;
|
|
34927
|
-
}
|
|
34928
|
-
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
34929
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
34930
|
-
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
34931
|
-
row[columns.length] = isAllStars(rows, i);
|
|
34932
|
-
for (let j = columns.length - 1; j >= 0; j--) {
|
|
34933
|
-
const rowStep = rows[i];
|
|
34934
|
-
const columnStep = columns[j];
|
|
34935
|
-
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
34936
|
-
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
34937
|
-
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
34938
|
-
continue;
|
|
34939
|
-
}
|
|
34940
|
-
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
34941
|
-
}
|
|
34942
|
-
next = row;
|
|
34943
|
-
}
|
|
34944
|
-
return next[0] ?? false;
|
|
34945
|
-
}
|
|
34946
|
-
//#endregion
|
|
34947
35823
|
//#region src/features/permissions/augmentcode-permissions.ts
|
|
34948
35824
|
const moduleLogger$2 = fallbackLogger;
|
|
34949
35825
|
z.enum([
|
|
@@ -35195,6 +36071,7 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35195
36071
|
const basicExistingEntries = existingEntries.filter((entry) => !isSpecialEntry(entry));
|
|
35196
36072
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
35197
36073
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
36074
|
+
if (entry.toolName === "*") return false;
|
|
35198
36075
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(entry.toolName)) return true;
|
|
35199
36076
|
if (entry.permission.type === "deny") {
|
|
35200
36077
|
const key = `${entry.toolName}|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
@@ -35272,7 +36149,18 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35272
36149
|
};
|
|
35273
36150
|
function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
35274
36151
|
const entries = [];
|
|
35275
|
-
|
|
36152
|
+
const resolvedBashRules = bashRulesHonoringAllTools(config.permission);
|
|
36153
|
+
const permission = config.permission.bash !== void 0 || Object.keys(resolvedBashRules).length > 0 ? {
|
|
36154
|
+
...config.permission,
|
|
36155
|
+
bash: resolvedBashRules
|
|
36156
|
+
} : config.permission;
|
|
36157
|
+
const allToolsFailClosedType = computeAllToolsFailClosedType(config.permission["*"]);
|
|
36158
|
+
const categoriesWithOwnEntries = /* @__PURE__ */ new Set();
|
|
36159
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
36160
|
+
if (category === "*") {
|
|
36161
|
+
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.");
|
|
36162
|
+
continue;
|
|
36163
|
+
}
|
|
35276
36164
|
const augmentToolName = toAugmentToolName(category);
|
|
35277
36165
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(augmentToolName) && augmentToolName === category) logger?.warn(`AugmentCode permissions: passing through unknown tool category '${category}' as toolName.`);
|
|
35278
36166
|
if (augmentToolName === "launch-process") {
|
|
@@ -35297,19 +36185,50 @@ function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
|
35297
36185
|
toolName: augmentToolName,
|
|
35298
36186
|
permission: { type: "deny" }
|
|
35299
36187
|
});
|
|
36188
|
+
categoriesWithOwnEntries.add(category);
|
|
35300
36189
|
continue;
|
|
35301
36190
|
}
|
|
35302
36191
|
const droppedPatterns = [];
|
|
35303
|
-
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*")
|
|
35304
|
-
|
|
35305
|
-
|
|
35306
|
-
|
|
35307
|
-
|
|
36192
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") {
|
|
36193
|
+
entries.push({
|
|
36194
|
+
toolName: augmentToolName,
|
|
36195
|
+
permission: { type: actionToAugmentType(action) }
|
|
36196
|
+
});
|
|
36197
|
+
categoriesWithOwnEntries.add(category);
|
|
36198
|
+
} else droppedPatterns.push(pattern);
|
|
35308
36199
|
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.`);
|
|
35309
36200
|
}
|
|
36201
|
+
entries.push(...synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType));
|
|
35310
36202
|
return entries;
|
|
35311
36203
|
}
|
|
35312
36204
|
/**
|
|
36205
|
+
* The strictest action the all-tools `*` category imposes, for tools with no per-input matcher to
|
|
36206
|
+
* narrow it onto (see {@link synthesizeManagedToolFallbackEntries}). `deny` wins over `ask`,
|
|
36207
|
+
* and an all-tools `allow` never forces an entry — there is nothing to fail closed on.
|
|
36208
|
+
*/
|
|
36209
|
+
function computeAllToolsFailClosedType(allToolsRules) {
|
|
36210
|
+
if (!allToolsRules) return void 0;
|
|
36211
|
+
const actions = Object.values(allToolsRules);
|
|
36212
|
+
if (actions.some((action) => action === "deny")) return "deny";
|
|
36213
|
+
if (actions.some((action) => action === "ask")) return "ask-user";
|
|
36214
|
+
}
|
|
36215
|
+
/**
|
|
36216
|
+
* Extend the same fail-closed treatment `bash` gets (via {@link bashRulesHonoringAllTools}) to the
|
|
36217
|
+
* other managed tools: one that produced no entries of its own above must not fall back to
|
|
36218
|
+
* AugmentCode's own default just because it has no per-input matcher to narrow the all-tools
|
|
36219
|
+
* restriction onto. A category can be stated yet still emit nothing (e.g. only non-`*` allow/ask
|
|
36220
|
+
* patterns, dropped with a warning), so this checks emitted entries rather than whether the
|
|
36221
|
+
* category was merely present in the source config. A tool whose own rules did emit entries is
|
|
36222
|
+
* left untouched here.
|
|
36223
|
+
*/
|
|
36224
|
+
function synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType) {
|
|
36225
|
+
if (allToolsFailClosedType === void 0) return [];
|
|
36226
|
+
return Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).filter(([canonicalName]) => canonicalName !== "bash" && !categoriesWithOwnEntries.has(canonicalName)).map(([, augmentToolName]) => ({
|
|
36227
|
+
toolName: augmentToolName,
|
|
36228
|
+
permission: { type: allToolsFailClosedType }
|
|
36229
|
+
}));
|
|
36230
|
+
}
|
|
36231
|
+
/**
|
|
35313
36232
|
* Sort AugmentCode tool-permission entries to make the `first-match-wins` semantics safe and predictable.
|
|
35314
36233
|
*
|
|
35315
36234
|
* Augment evaluates `toolPermissions` top-to-bottom and stops at the first match. To prevent a
|
|
@@ -35404,216 +36323,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
35404
36323
|
}
|
|
35405
36324
|
return { permission };
|
|
35406
36325
|
}
|
|
35407
|
-
/**
|
|
35408
|
-
* Collect the canonical rules that govern shell commands, for the adapters
|
|
35409
|
-
* whose tool models commands and nothing else.
|
|
35410
|
-
*
|
|
35411
|
-
* The `bash` category contributes every rule. The all-tools `*` category
|
|
35412
|
-
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
35413
|
-
* written there covers shell commands too, and dropping it inverts the
|
|
35414
|
-
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
35415
|
-
* an adapter that reads only `bash` auto-approves the very command the file
|
|
35416
|
-
* denies.
|
|
35417
|
-
*
|
|
35418
|
-
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35419
|
-
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35420
|
-
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35421
|
-
* it in the permissive direction would grant something the author never said
|
|
35422
|
-
* about commands. Both directions therefore fail closed.
|
|
35423
|
-
*/
|
|
35424
|
-
function collectShellCommandRules(permission) {
|
|
35425
|
-
const rules = [];
|
|
35426
|
-
const foreignRestrictingCategories = [];
|
|
35427
|
-
const ignoredAllToolsAllowPatterns = [];
|
|
35428
|
-
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35429
|
-
if (category === "bash") {
|
|
35430
|
-
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35431
|
-
pattern,
|
|
35432
|
-
action,
|
|
35433
|
-
fromAllToolsCategory: false
|
|
35434
|
-
});
|
|
35435
|
-
continue;
|
|
35436
|
-
}
|
|
35437
|
-
if (category === "*") {
|
|
35438
|
-
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35439
|
-
if (action === "allow") {
|
|
35440
|
-
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35441
|
-
continue;
|
|
35442
|
-
}
|
|
35443
|
-
rules.push({
|
|
35444
|
-
pattern,
|
|
35445
|
-
action,
|
|
35446
|
-
fromAllToolsCategory: true
|
|
35447
|
-
});
|
|
35448
|
-
}
|
|
35449
|
-
continue;
|
|
35450
|
-
}
|
|
35451
|
-
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35452
|
-
}
|
|
35453
|
-
return {
|
|
35454
|
-
rules,
|
|
35455
|
-
foreignRestrictingCategories,
|
|
35456
|
-
ignoredAllToolsAllowPatterns
|
|
35457
|
-
};
|
|
35458
|
-
}
|
|
35459
|
-
/**
|
|
35460
|
-
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35461
|
-
* which restrictions it cannot write name some of the same commands? The
|
|
35462
|
-
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35463
|
-
* written — so a caller can report both the allow rules it withheld and the
|
|
35464
|
-
* restrictions that withheld nothing.
|
|
35465
|
-
*
|
|
35466
|
-
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35467
|
-
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35468
|
-
* asking whether any one command matches both. Width does not enter into it: an
|
|
35469
|
-
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35470
|
-
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35471
|
-
* on every `git ... --force` command even though neither pattern covers the
|
|
35472
|
-
* other's spelling. Comparing only identical spellings would let the most
|
|
35473
|
-
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35474
|
-
*
|
|
35475
|
-
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35476
|
-
* the walk for the commonest case.
|
|
35477
|
-
*
|
|
35478
|
-
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35479
|
-
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35480
|
-
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35481
|
-
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35482
|
-
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35483
|
-
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35484
|
-
* withholds an allow rather than writing one the config restricts — see
|
|
35485
|
-
* `warpCommandPatternToGlob`.
|
|
35486
|
-
*/
|
|
35487
|
-
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35488
|
-
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35489
|
-
pattern,
|
|
35490
|
-
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35491
|
-
}));
|
|
35492
|
-
return (allowPattern) => {
|
|
35493
|
-
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35494
|
-
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35495
|
-
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35496
|
-
};
|
|
35497
|
-
}
|
|
35498
|
-
/**
|
|
35499
|
-
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35500
|
-
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35501
|
-
*
|
|
35502
|
-
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35503
|
-
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35504
|
-
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35505
|
-
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35506
|
-
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35507
|
-
*
|
|
35508
|
-
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35509
|
-
* so overlapping no allow rule says nothing is wrong with it.
|
|
35510
|
-
*/
|
|
35511
|
-
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35512
|
-
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35513
|
-
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35514
|
-
return uniq(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35515
|
-
}
|
|
35516
|
-
/**
|
|
35517
|
-
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35518
|
-
* commands with those two tiers and nothing else.
|
|
35519
|
-
*
|
|
35520
|
-
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35521
|
-
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35522
|
-
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35523
|
-
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35524
|
-
* about would answer the prompt the author wanted.
|
|
35525
|
-
*
|
|
35526
|
-
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35527
|
-
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35528
|
-
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35529
|
-
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35530
|
-
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35531
|
-
* cannot be written it withholds the allow rules it covers instead, which
|
|
35532
|
-
* restricts in the same direction without touching the denylist.
|
|
35533
|
-
*
|
|
35534
|
-
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35535
|
-
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35536
|
-
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35537
|
-
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35538
|
-
* even where it is written: a pattern under `*` need not name a command —
|
|
35539
|
-
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35540
|
-
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35541
|
-
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35542
|
-
* command pattern is reported; failing open would not be.
|
|
35543
|
-
*
|
|
35544
|
-
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35545
|
-
* patterns are not globs.
|
|
35546
|
-
*/
|
|
35547
|
-
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35548
|
-
const deny = [];
|
|
35549
|
-
const unwrittenDenyPatterns = [];
|
|
35550
|
-
const restrictions = [];
|
|
35551
|
-
const writtenAllToolsDenyPatterns = [];
|
|
35552
|
-
const allToolsAskPatterns = [];
|
|
35553
|
-
for (const rule of rules) {
|
|
35554
|
-
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35555
|
-
if (action === "allow") continue;
|
|
35556
|
-
if (action !== "deny") {
|
|
35557
|
-
restrictions.push(rule);
|
|
35558
|
-
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35559
|
-
continue;
|
|
35560
|
-
}
|
|
35561
|
-
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35562
|
-
deny.push(pattern);
|
|
35563
|
-
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35564
|
-
} else unwrittenDenyPatterns.push(pattern);
|
|
35565
|
-
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35566
|
-
}
|
|
35567
|
-
const budget = createIntersectionBudget();
|
|
35568
|
-
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35569
|
-
normalizePattern,
|
|
35570
|
-
budget
|
|
35571
|
-
});
|
|
35572
|
-
const allow = [];
|
|
35573
|
-
const shadowedAllowPatterns = [];
|
|
35574
|
-
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35575
|
-
for (const { pattern, action } of rules) {
|
|
35576
|
-
if (action !== "allow") continue;
|
|
35577
|
-
const shadowing = shadowingRestrictions(pattern);
|
|
35578
|
-
if (shadowing.length > 0) {
|
|
35579
|
-
shadowedAllowPatterns.push(pattern);
|
|
35580
|
-
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35581
|
-
continue;
|
|
35582
|
-
}
|
|
35583
|
-
allow.push(pattern);
|
|
35584
|
-
}
|
|
35585
|
-
return {
|
|
35586
|
-
allow,
|
|
35587
|
-
deny,
|
|
35588
|
-
shadowedAllowPatterns,
|
|
35589
|
-
unwrittenDenyPatterns,
|
|
35590
|
-
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35591
|
-
rules,
|
|
35592
|
-
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35593
|
-
withholdingPatterns
|
|
35594
|
-
}),
|
|
35595
|
-
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35596
|
-
rules,
|
|
35597
|
-
allToolsPatterns: allToolsAskPatterns,
|
|
35598
|
-
withholdingPatterns
|
|
35599
|
-
}),
|
|
35600
|
-
intersectionBudgetExhausted: budget.remaining === 0
|
|
35601
|
-
};
|
|
35602
|
-
}
|
|
35603
|
-
/**
|
|
35604
|
-
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35605
|
-
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35606
|
-
* dropped in one is worded the same way in all.
|
|
35607
|
-
*/
|
|
35608
|
-
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35609
|
-
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.`);
|
|
35610
|
-
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.`);
|
|
35611
|
-
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.`);
|
|
35612
|
-
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.`);
|
|
35613
|
-
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.`);
|
|
35614
|
-
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.`);
|
|
35615
|
-
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.`);
|
|
35616
|
-
}
|
|
35617
36326
|
//#endregion
|
|
35618
36327
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
35619
36328
|
/**
|
|
@@ -37375,7 +38084,7 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
|
|
|
37375
38084
|
return merged;
|
|
37376
38085
|
}
|
|
37377
38086
|
function buildCodexBashRulesContent(config) {
|
|
37378
|
-
const bashRules = config.permission
|
|
38087
|
+
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
37379
38088
|
const entries = Object.entries(bashRules);
|
|
37380
38089
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
37381
38090
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
@@ -38066,7 +38775,7 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
38066
38775
|
function convertRulesyncToCursorPermissions(config, logger) {
|
|
38067
38776
|
const allow = [];
|
|
38068
38777
|
const deny = [];
|
|
38069
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
38778
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38070
38779
|
const cursorType = toCursorType(category);
|
|
38071
38780
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38072
38781
|
const entry = buildCursorPermissionEntry(cursorType, toCursorPattern(category, pattern));
|
|
@@ -38885,7 +39594,7 @@ function convertRulesyncToDevinPermissions(config) {
|
|
|
38885
39594
|
const allow = [];
|
|
38886
39595
|
const ask = [];
|
|
38887
39596
|
const deny = [];
|
|
38888
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
39597
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38889
39598
|
const scope = toDevinScope(category);
|
|
38890
39599
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38891
39600
|
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
@@ -39173,6 +39882,19 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
39173
39882
|
isDeletable() {
|
|
39174
39883
|
return false;
|
|
39175
39884
|
}
|
|
39885
|
+
/**
|
|
39886
|
+
* `permission.yaml` is Goose's file, not one rulesync owns: rulesync merges
|
|
39887
|
+
* into it when it exists but has no business bringing it into existence to
|
|
39888
|
+
* hold nothing. When no rule maps, the `user` block holds three empty lists,
|
|
39889
|
+
* which would otherwise be written as a fresh permission.yaml that says
|
|
39890
|
+
* nothing — an absent file and empty lists both mean "no user override, so
|
|
39891
|
+
* Goose decides on its own". An existing file is still rewritten as before,
|
|
39892
|
+
* so user content is never dropped — the skip only applies when there is no
|
|
39893
|
+
* file yet.
|
|
39894
|
+
*/
|
|
39895
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
39896
|
+
return true;
|
|
39897
|
+
}
|
|
39176
39898
|
static getSettablePaths(_options) {
|
|
39177
39899
|
return {
|
|
39178
39900
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
@@ -39257,7 +39979,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
39257
39979
|
never_allow: []
|
|
39258
39980
|
};
|
|
39259
39981
|
const assigned = /* @__PURE__ */ new Map();
|
|
39260
|
-
const orderedEntries = Object.entries(config.permission).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39982
|
+
const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39261
39983
|
for (const [category, rules] of orderedEntries) {
|
|
39262
39984
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
39263
39985
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -39589,7 +40311,7 @@ function unmanagedEntries(existingPermission, key) {
|
|
|
39589
40311
|
*/
|
|
39590
40312
|
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
39591
40313
|
const ranked = /* @__PURE__ */ new Map();
|
|
39592
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
40314
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
39593
40315
|
const entry = buildGrokEntry(category, pattern);
|
|
39594
40316
|
if (entry === null) {
|
|
39595
40317
|
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.`);
|
|
@@ -39678,6 +40400,8 @@ function deriveGrokPermissionMode(config) {
|
|
|
39678
40400
|
function patternsByAction(category, action) {
|
|
39679
40401
|
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
39680
40402
|
}
|
|
40403
|
+
/** The canonical category whose `deny` rules feed `security.website_blocklist`. */
|
|
40404
|
+
const WEBFETCH_PERMISSION_CATEGORY = "webfetch";
|
|
39681
40405
|
function clonePermissionBlock(permission) {
|
|
39682
40406
|
return Object.fromEntries(Object.entries(permission).map(([category, rules]) => [category, { ...rules }]));
|
|
39683
40407
|
}
|
|
@@ -39690,18 +40414,39 @@ function ensureCategory(permission, category) {
|
|
|
39690
40414
|
function removeEmptyCategories(permission) {
|
|
39691
40415
|
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
39692
40416
|
}
|
|
40417
|
+
/**
|
|
40418
|
+
* Make the native `command_allowlist` authoritative for the `bash` allow rules
|
|
40419
|
+
* it can speak for, and for those only. The allowlist is a list of shell-command
|
|
40420
|
+
* patterns, so it is generated from `bash` alone (see `fromRulesyncPermissions`);
|
|
40421
|
+
* an allow in any other category names something Hermes's allowlist cannot
|
|
40422
|
+
* carry, so its absence from the list says nothing about it and it is kept as
|
|
40423
|
+
* provenance wrote it. The same holds for a `bash` allow the generator withheld
|
|
40424
|
+
* because a stricter `*` or `bash` rule covers it: the allowlist never carried
|
|
40425
|
+
* it, so its absence is not a retraction and the rule is kept too.
|
|
40426
|
+
*/
|
|
39693
40427
|
function reconcileCommandAllowlist({ permission, commandAllowlist }) {
|
|
39694
|
-
const
|
|
39695
|
-
const
|
|
39696
|
-
|
|
39697
|
-
|
|
39698
|
-
|
|
39699
|
-
|
|
39700
|
-
|
|
39701
|
-
for (const pattern of
|
|
39702
|
-
|
|
39703
|
-
|
|
39704
|
-
|
|
40428
|
+
const { rules: commandRules } = collectShellCommandRules(permission);
|
|
40429
|
+
const { shadowedAllowPatterns } = partitionCommandRules({
|
|
40430
|
+
rules: commandRules,
|
|
40431
|
+
writesAllToolsDeny: false
|
|
40432
|
+
});
|
|
40433
|
+
const withheld = new Set(shadowedAllowPatterns);
|
|
40434
|
+
const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
|
|
40435
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
|
|
40436
|
+
for (const pattern of commandAllowlist) rules[pattern] = "allow";
|
|
40437
|
+
}
|
|
40438
|
+
/**
|
|
40439
|
+
* Report the restricting rules Hermes has no per-pattern primitive for: a
|
|
40440
|
+
* `deny` or `ask` in any category other than `bash`, `*`, and `webfetch`, and
|
|
40441
|
+
* an `ask` under `webfetch` — the blocklist carries a `webfetch` deny but has
|
|
40442
|
+
* no ask tier. (`bash` and `*` are reported by `warnAboutUnwrittenCommandRules`.)
|
|
40443
|
+
* Such rules survive only in the round-trip blob.
|
|
40444
|
+
*/
|
|
40445
|
+
function warnAboutUnexpressedHermesRestrictions({ permissionBlock, foreignRestrictingCategories, logger }) {
|
|
40446
|
+
for (const category of foreignRestrictingCategories) {
|
|
40447
|
+
const isWebfetch = category === WEBFETCH_PERMISSION_CATEGORY;
|
|
40448
|
+
if (isWebfetch && patternsByAction(permissionBlock[category], "ask").length === 0) continue;
|
|
40449
|
+
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.`);
|
|
39705
40450
|
}
|
|
39706
40451
|
}
|
|
39707
40452
|
function reconcileNativeDenies({ permission, category, patterns }) {
|
|
@@ -39813,14 +40558,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39813
40558
|
const approvals = isRecord$1(config.approvals) ? config.approvals : {};
|
|
39814
40559
|
reconcileNativeDenies({
|
|
39815
40560
|
permission,
|
|
39816
|
-
category:
|
|
40561
|
+
category: SHELL_PERMISSION_CATEGORY,
|
|
39817
40562
|
patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
|
|
39818
40563
|
});
|
|
39819
40564
|
const security = isRecord$1(config.security) ? config.security : {};
|
|
39820
40565
|
const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
|
|
39821
40566
|
reconcileNativeDenies({
|
|
39822
40567
|
permission,
|
|
39823
|
-
category:
|
|
40568
|
+
category: WEBFETCH_PERMISSION_CATEGORY,
|
|
39824
40569
|
patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
|
|
39825
40570
|
});
|
|
39826
40571
|
removeEmptyCategories(permission);
|
|
@@ -39840,12 +40585,32 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39840
40585
|
fileContent: JSON.stringify(imported, null, 2)
|
|
39841
40586
|
});
|
|
39842
40587
|
}
|
|
39843
|
-
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
|
|
40588
|
+
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false, logger }) {
|
|
39844
40589
|
const permissions = rulesyncPermissions.getJson();
|
|
39845
40590
|
const permissionBlock = permissions.permission ?? {};
|
|
39846
|
-
const
|
|
39847
|
-
const bashDeny =
|
|
39848
|
-
|
|
40591
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permissionBlock);
|
|
40592
|
+
const { allow: commandAllowlist, deny: bashDeny, shadowedAllowPatterns, unwrittenDenyPatterns, unenforcedAllToolsAskPatterns, intersectionBudgetExhausted } = partitionCommandRules({
|
|
40593
|
+
rules,
|
|
40594
|
+
writesAllToolsDeny: false
|
|
40595
|
+
});
|
|
40596
|
+
warnAboutUnexpressedHermesRestrictions({
|
|
40597
|
+
permissionBlock,
|
|
40598
|
+
foreignRestrictingCategories,
|
|
40599
|
+
logger
|
|
40600
|
+
});
|
|
40601
|
+
warnAboutUnwrittenCommandRules({
|
|
40602
|
+
toolLabel: "Hermes Agent",
|
|
40603
|
+
surfaceLabel: "command_allowlist/approvals.deny",
|
|
40604
|
+
foreignRestrictingCategories: [],
|
|
40605
|
+
shadowedAllowPatterns,
|
|
40606
|
+
unwrittenDenyPatterns,
|
|
40607
|
+
unwrittenDenyReason: "approvals.deny is a hard denylist of shell commands, and a pattern written under '*' need not be a command at all.",
|
|
40608
|
+
unenforcedAllToolsAskPatterns,
|
|
40609
|
+
ignoredAllToolsAllowPatterns,
|
|
40610
|
+
intersectionBudgetExhausted,
|
|
40611
|
+
logger
|
|
40612
|
+
});
|
|
40613
|
+
const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
|
|
39849
40614
|
let config = {};
|
|
39850
40615
|
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
39851
40616
|
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
@@ -40112,7 +40877,7 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
40112
40877
|
*/
|
|
40113
40878
|
function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
|
|
40114
40879
|
const ruleLists = {};
|
|
40115
|
-
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
40880
|
+
for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
40116
40881
|
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
40117
40882
|
if (!group) {
|
|
40118
40883
|
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.`);
|
|
@@ -40362,7 +41127,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
40362
41127
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
40363
41128
|
const kiloOverride = rulesyncJson.kilo;
|
|
40364
41129
|
const incomingPermission = {
|
|
40365
|
-
...rulesyncJson.permission,
|
|
41130
|
+
...honorAllToolsOnBash(rulesyncJson.permission),
|
|
40366
41131
|
...kiloOverride?.permission
|
|
40367
41132
|
};
|
|
40368
41133
|
const droppedDenyByKey = {};
|
|
@@ -40881,7 +41646,7 @@ function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
|
40881
41646
|
allowedCommands: [],
|
|
40882
41647
|
deniedCommands: []
|
|
40883
41648
|
};
|
|
40884
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
41649
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
40885
41650
|
if (action === "ask") {
|
|
40886
41651
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
40887
41652
|
continue;
|
|
@@ -41165,7 +41930,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
41165
41930
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
41166
41931
|
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
41167
41932
|
const sharedPermission = {};
|
|
41168
|
-
for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41933
|
+
for (const [category, value] of Object.entries(honorAllToolsOnBash(rulesyncJson.permission ?? {}))) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41169
41934
|
const permission = {};
|
|
41170
41935
|
for (const [category, value] of Object.entries({
|
|
41171
41936
|
...sharedPermission,
|
|
@@ -42294,11 +43059,19 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42294
43059
|
const paths = this.getSettablePaths();
|
|
42295
43060
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
42296
43061
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
42297
|
-
const
|
|
43062
|
+
const permission = rulesyncPermissions.getJson().permission;
|
|
43063
|
+
const bashStated = permission[COMMAND_CATEGORY] !== void 0;
|
|
42298
43064
|
const patch = {};
|
|
42299
|
-
if (
|
|
43065
|
+
if (bashStated) {
|
|
43066
|
+
const { bash } = resolveShellCommandLists({
|
|
43067
|
+
permission,
|
|
43068
|
+
writesAllToolsDeny: true,
|
|
43069
|
+
toolLabel: this.getToolLabel(),
|
|
43070
|
+
surfaceLabel: `${this.getAllowedCommandsKey()}/${this.getDeniedCommandsKey()}`,
|
|
43071
|
+
logger
|
|
43072
|
+
});
|
|
42300
43073
|
const { allowed, denied } = buildVscodeCommandLists({
|
|
42301
|
-
rules,
|
|
43074
|
+
rules: bash,
|
|
42302
43075
|
toolLabel: this.getToolLabel(),
|
|
42303
43076
|
logger
|
|
42304
43077
|
});
|
|
@@ -42309,7 +43082,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42309
43082
|
outputRoot,
|
|
42310
43083
|
relativeDirPath: paths.relativeDirPath,
|
|
42311
43084
|
relativeFilePath: paths.relativeFilePath,
|
|
42312
|
-
ownsCommandKeys:
|
|
43085
|
+
ownsCommandKeys: bashStated,
|
|
42313
43086
|
fileContent: applySharedConfigPatch({
|
|
42314
43087
|
fileKey: sharedConfigFileKey(paths),
|
|
42315
43088
|
feature: "permissions",
|
|
@@ -42689,7 +43462,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
42689
43462
|
config,
|
|
42690
43463
|
logger
|
|
42691
43464
|
});
|
|
42692
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
43465
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42693
43466
|
if (category === CATCH_ALL_PATTERN$1) {
|
|
42694
43467
|
const toolWideDefault = convertAllToolsRules({
|
|
42695
43468
|
rules,
|
|
@@ -43699,7 +44472,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
43699
44472
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43700
44473
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
43701
44474
|
const config = parseVibeConfig(existingContent);
|
|
43702
|
-
const permission = rulesyncPermissions.getJson().permission;
|
|
44475
|
+
const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
|
|
43703
44476
|
const vibeOverride = rulesyncPermissions.getJson().vibe;
|
|
43704
44477
|
const tools = toVibeToolsRecord(config.tools);
|
|
43705
44478
|
const diskShellPatterns = new Map([VIBE_SHELL_CATEGORY, ...VIBE_SHELL_ALIAS_TOOL_NAMES].map((vibeToolName) => [vibeToolName, toStringArray(readVibeToolConfig({
|
|
@@ -45040,6 +45813,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
45040
45813
|
isDeletable() {
|
|
45041
45814
|
return false;
|
|
45042
45815
|
}
|
|
45816
|
+
/**
|
|
45817
|
+
* `settings.toml` is Warp's file, not one rulesync owns: rulesync merges into
|
|
45818
|
+
* it when it exists but has no business bringing it into existence to hold
|
|
45819
|
+
* nothing. When no rule maps, both command lists are dropped and the payload
|
|
45820
|
+
* is a bare `[agents.profiles]` table, which would otherwise be written as a
|
|
45821
|
+
* fresh settings file that says nothing — an absent file and an empty table
|
|
45822
|
+
* both mean "Warp's own defaults". An existing file is still rewritten as
|
|
45823
|
+
* before, so user content is never dropped — the skip only applies when
|
|
45824
|
+
* there is no file yet.
|
|
45825
|
+
*/
|
|
45826
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
45827
|
+
return true;
|
|
45828
|
+
}
|
|
45043
45829
|
static getSettablePaths(_options) {
|
|
45044
45830
|
return {
|
|
45045
45831
|
relativeDirPath: warpSettingsDir(),
|
|
@@ -45651,6 +46437,7 @@ function buildZedToolPermissions({ permission, logger }) {
|
|
|
45651
46437
|
for (const [category, rules] of Object.entries(permission)) {
|
|
45652
46438
|
if (category === "*") {
|
|
45653
46439
|
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
46440
|
+
else if (permission.bash?.[pattern] === "deny" || permission.bash?.[pattern] === "ask") continue;
|
|
45654
46441
|
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.`);
|
|
45655
46442
|
continue;
|
|
45656
46443
|
}
|
|
@@ -45805,7 +46592,7 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
45805
46592
|
const toolPermissions = asRecord(agent.tool_permissions);
|
|
45806
46593
|
const existingTools = asRecord(toolPermissions.tools);
|
|
45807
46594
|
const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
|
|
45808
|
-
permission: config.permission,
|
|
46595
|
+
permission: honorAllToolsOnBash(config.permission),
|
|
45809
46596
|
logger
|
|
45810
46597
|
});
|
|
45811
46598
|
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\`.`);
|
|
@@ -46314,6 +47101,24 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
46314
47101
|
}
|
|
46315
47102
|
};
|
|
46316
47103
|
//#endregion
|
|
47104
|
+
//#region src/constants/codebuddy-paths.ts
|
|
47105
|
+
/**
|
|
47106
|
+
* CodeBuddy Code configuration-layout conventions.
|
|
47107
|
+
*
|
|
47108
|
+
* CodeBuddy Code (`@tencent-ai/codebuddy-code`) is Tencent Cloud's terminal
|
|
47109
|
+
* coding agent. Its configuration surface mirrors Claude Code closely: a
|
|
47110
|
+
* root memory file plus a `.codebuddy/` tree.
|
|
47111
|
+
*
|
|
47112
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
47113
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
47114
|
+
*/
|
|
47115
|
+
/** Root directory for CodeBuddy Code configuration, relative to the scope root. */
|
|
47116
|
+
const CODEBUDDY_DIR = ".codebuddy";
|
|
47117
|
+
const CODEBUDDY_RULE_FILE_NAME = "CODEBUDDY.md";
|
|
47118
|
+
const CODEBUDDY_LOCAL_RULE_FILE_NAME = "CODEBUDDY.local.md";
|
|
47119
|
+
/** Modular rules directory name under `.codebuddy/`. */
|
|
47120
|
+
const CODEBUDDY_RULES_DIR_NAME = "rules";
|
|
47121
|
+
//#endregion
|
|
46317
47122
|
//#region src/features/skills/simulated-skill.ts
|
|
46318
47123
|
const SimulatedSkillFrontmatterSchema = z.looseObject({
|
|
46319
47124
|
name: z.string(),
|
|
@@ -46852,7 +47657,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46852
47657
|
* This only deletes directories that are no longer in the rulesync source, not directories that will be overwritten.
|
|
46853
47658
|
*/
|
|
46854
47659
|
async removeOrphanAiDirs(existingDirs, generatedDirs) {
|
|
46855
|
-
const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
|
|
47660
|
+
const generatedPaths = new Set(generatedDirs.map((d) => caseFoldIdentity(d.getDirPath())));
|
|
46856
47661
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46857
47662
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46858
47663
|
for (const aiDir of existingDirs) {
|
|
@@ -46877,7 +47682,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46877
47682
|
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`);
|
|
46878
47683
|
continue;
|
|
46879
47684
|
}
|
|
46880
|
-
if (!generatedPaths.has(dirPath)) orphanPaths.add(dirPath);
|
|
47685
|
+
if (!generatedPaths.has(caseFoldIdentity(dirPath))) orphanPaths.add(dirPath);
|
|
46881
47686
|
}
|
|
46882
47687
|
return await this.deleteOrphanPaths({
|
|
46883
47688
|
paths: orphanPaths,
|
|
@@ -46976,7 +47781,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46976
47781
|
const mainFile = aiDir.getMainFile();
|
|
46977
47782
|
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
46978
47783
|
for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
|
|
46979
|
-
const generatedNamesFolded = new Set([...generatedNames].map((name) => name
|
|
47784
|
+
const generatedNamesFolded = new Set([...generatedNames].map((name) => caseFoldIdentity(name)));
|
|
46980
47785
|
let existingNames;
|
|
46981
47786
|
try {
|
|
46982
47787
|
existingNames = await listFilePathsRecursively(dirPath, {
|
|
@@ -46991,7 +47796,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46991
47796
|
const posixName = toPosixPath(existingName);
|
|
46992
47797
|
if (generatedNames.has(posixName)) continue;
|
|
46993
47798
|
const filePath = join(dirPath, existingName);
|
|
46994
|
-
if (generatedNamesFolded.has(posixName
|
|
47799
|
+
if (generatedNamesFolded.has(caseFoldIdentity(posixName))) {
|
|
46995
47800
|
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`);
|
|
46996
47801
|
continue;
|
|
46997
47802
|
}
|
|
@@ -47047,7 +47852,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47047
47852
|
generatedPaths.add(flatFilePath);
|
|
47048
47853
|
for (const file of generatedDir.getOtherFiles()) generatedPaths.add(join(generatedDirPath, file.relativeFilePathToDirPath));
|
|
47049
47854
|
}
|
|
47050
|
-
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath
|
|
47855
|
+
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => caseFoldIdentity(generatedPath)));
|
|
47051
47856
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
47052
47857
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
47053
47858
|
for (const aiDir of existingFlatFiles) {
|
|
@@ -47077,7 +47882,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47077
47882
|
continue;
|
|
47078
47883
|
}
|
|
47079
47884
|
if (generatedPaths.has(filePath)) continue;
|
|
47080
|
-
if (generatedPathsFolded.has(filePath
|
|
47885
|
+
if (generatedPathsFolded.has(caseFoldIdentity(filePath))) {
|
|
47081
47886
|
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`);
|
|
47082
47887
|
continue;
|
|
47083
47888
|
}
|
|
@@ -48909,6 +49714,191 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
48909
49714
|
}
|
|
48910
49715
|
};
|
|
48911
49716
|
//#endregion
|
|
49717
|
+
//#region src/features/skills/crush-skill.ts
|
|
49718
|
+
const CrushSkillFrontmatterSchema = z.looseObject({
|
|
49719
|
+
name: z.string(),
|
|
49720
|
+
description: z.string(),
|
|
49721
|
+
"user-invocable": z.optional(z.boolean()),
|
|
49722
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
49723
|
+
license: z.optional(z.string()),
|
|
49724
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
49725
|
+
metadata: z.optional(z.looseObject({}))
|
|
49726
|
+
});
|
|
49727
|
+
/**
|
|
49728
|
+
* Represents a Crush Agent Skill directory.
|
|
49729
|
+
*
|
|
49730
|
+
* Crush auto-discovers Agent Skills (`SKILL.md` per directory) from
|
|
49731
|
+
* `.crush/skills/` at project scope and `~/.config/crush/skills/` (or
|
|
49732
|
+
* `$CRUSH_SKILLS_DIR`) at global scope. Unless `$CRUSH_SKILLS_DIR` is set,
|
|
49733
|
+
* Crush also scans several shared directories it does not own (globally
|
|
49734
|
+
* `~/.config/agents/skills/`, `~/.agents/skills/`, `~/.claude/skills/`;
|
|
49735
|
+
* per-project `.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, also
|
|
49736
|
+
* checked at a git worktree's common root); this class writes only to the
|
|
49737
|
+
* Crush-specific path above, leaving those shared roots to their own targets.
|
|
49738
|
+
*
|
|
49739
|
+
* Crush's `UserInvocable` field is a non-pointer Go `bool`, so an omitted
|
|
49740
|
+
* `user-invocable` (at both the root and the `crush:` section) resolves to
|
|
49741
|
+
* `false`: the skill stays reachable by the model but is hidden from Crush's
|
|
49742
|
+
* command palette. See `FromSkillCatalog` in `internal/commands/commands.go`.
|
|
49743
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
49744
|
+
*/
|
|
49745
|
+
var CrushSkill = class CrushSkill extends ToolSkill {
|
|
49746
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = CRUSH_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
49747
|
+
super({
|
|
49748
|
+
outputRoot,
|
|
49749
|
+
relativeDirPath,
|
|
49750
|
+
dirName,
|
|
49751
|
+
mainFile: {
|
|
49752
|
+
name: SKILL_FILE_NAME,
|
|
49753
|
+
body,
|
|
49754
|
+
frontmatter: { ...frontmatter }
|
|
49755
|
+
},
|
|
49756
|
+
otherFiles,
|
|
49757
|
+
global
|
|
49758
|
+
});
|
|
49759
|
+
if (validate) {
|
|
49760
|
+
const result = this.validate();
|
|
49761
|
+
if (!result.success) throw result.error;
|
|
49762
|
+
}
|
|
49763
|
+
}
|
|
49764
|
+
static getSettablePaths({ global = false } = {}) {
|
|
49765
|
+
return { relativeDirPath: global ? CRUSH_SKILLS_GLOBAL_DIR : CRUSH_SKILLS_PROJECT_DIR };
|
|
49766
|
+
}
|
|
49767
|
+
getFrontmatter() {
|
|
49768
|
+
return CrushSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
49769
|
+
}
|
|
49770
|
+
getBody() {
|
|
49771
|
+
return this.mainFile?.body ?? "";
|
|
49772
|
+
}
|
|
49773
|
+
validate() {
|
|
49774
|
+
if (!this.mainFile) return {
|
|
49775
|
+
success: false,
|
|
49776
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
49777
|
+
};
|
|
49778
|
+
const result = CrushSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
49779
|
+
if (!result.success) return {
|
|
49780
|
+
success: false,
|
|
49781
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
49782
|
+
};
|
|
49783
|
+
return {
|
|
49784
|
+
success: true,
|
|
49785
|
+
error: null
|
|
49786
|
+
};
|
|
49787
|
+
}
|
|
49788
|
+
toRulesyncSkill() {
|
|
49789
|
+
const frontmatter = this.getFrontmatter();
|
|
49790
|
+
const crushSection = {
|
|
49791
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
49792
|
+
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
49793
|
+
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
49794
|
+
...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
|
|
49795
|
+
...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
|
|
49796
|
+
};
|
|
49797
|
+
const rulesyncFrontmatter = {
|
|
49798
|
+
name: frontmatter.name,
|
|
49799
|
+
description: frontmatter.description,
|
|
49800
|
+
targets: ["*"],
|
|
49801
|
+
...Object.keys(crushSection).length > 0 && { crush: crushSection }
|
|
49802
|
+
};
|
|
49803
|
+
return new RulesyncSkill({
|
|
49804
|
+
outputRoot: this.outputRoot,
|
|
49805
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
49806
|
+
dirName: this.getDirName(),
|
|
49807
|
+
frontmatter: rulesyncFrontmatter,
|
|
49808
|
+
body: this.getBody(),
|
|
49809
|
+
otherFiles: this.getOtherFiles(),
|
|
49810
|
+
validate: true,
|
|
49811
|
+
global: this.global
|
|
49812
|
+
});
|
|
49813
|
+
}
|
|
49814
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49815
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49816
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49817
|
+
const crushSection = rulesyncFrontmatter.crush;
|
|
49818
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
49819
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49820
|
+
section: crushSection
|
|
49821
|
+
});
|
|
49822
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
49823
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49824
|
+
section: crushSection
|
|
49825
|
+
});
|
|
49826
|
+
const license = resolveLicense({
|
|
49827
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49828
|
+
section: crushSection
|
|
49829
|
+
});
|
|
49830
|
+
const compatibility = resolveCompatibility({
|
|
49831
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49832
|
+
section: crushSection
|
|
49833
|
+
});
|
|
49834
|
+
const metadata = resolveMetadata({
|
|
49835
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49836
|
+
section: crushSection
|
|
49837
|
+
});
|
|
49838
|
+
const compatibilityString = compatibility === void 0 ? void 0 : toCompatibilityString(compatibility);
|
|
49839
|
+
const crushFrontmatter = {
|
|
49840
|
+
name: rulesyncFrontmatter.name,
|
|
49841
|
+
description: rulesyncFrontmatter.description,
|
|
49842
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
49843
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
49844
|
+
...license !== void 0 && { license },
|
|
49845
|
+
...compatibilityString !== void 0 && compatibilityString.length > 0 && { compatibility: compatibilityString },
|
|
49846
|
+
...metadata !== void 0 && { metadata: toStringMetadata(metadata) }
|
|
49847
|
+
};
|
|
49848
|
+
return new CrushSkill({
|
|
49849
|
+
outputRoot,
|
|
49850
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
49851
|
+
dirName: rulesyncSkill.getDirName(),
|
|
49852
|
+
frontmatter: crushFrontmatter,
|
|
49853
|
+
body: rulesyncSkill.getBody(),
|
|
49854
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
49855
|
+
validate,
|
|
49856
|
+
global
|
|
49857
|
+
});
|
|
49858
|
+
}
|
|
49859
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
49860
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
49861
|
+
return targets.includes("*") || targets.includes("crush");
|
|
49862
|
+
}
|
|
49863
|
+
static async fromDir(params) {
|
|
49864
|
+
const loaded = await this.loadSkillDirContent({
|
|
49865
|
+
...params,
|
|
49866
|
+
getSettablePaths: CrushSkill.getSettablePaths
|
|
49867
|
+
});
|
|
49868
|
+
const result = CrushSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
49869
|
+
if (!result.success) {
|
|
49870
|
+
const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
49871
|
+
throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
49872
|
+
}
|
|
49873
|
+
return new CrushSkill({
|
|
49874
|
+
outputRoot: loaded.outputRoot,
|
|
49875
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
49876
|
+
dirName: loaded.dirName,
|
|
49877
|
+
frontmatter: result.data,
|
|
49878
|
+
body: loaded.body,
|
|
49879
|
+
otherFiles: loaded.otherFiles,
|
|
49880
|
+
validate: true,
|
|
49881
|
+
global: loaded.global
|
|
49882
|
+
});
|
|
49883
|
+
}
|
|
49884
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
49885
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49886
|
+
return new CrushSkill({
|
|
49887
|
+
outputRoot,
|
|
49888
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
49889
|
+
dirName,
|
|
49890
|
+
frontmatter: {
|
|
49891
|
+
name: "",
|
|
49892
|
+
description: ""
|
|
49893
|
+
},
|
|
49894
|
+
body: "",
|
|
49895
|
+
otherFiles: [],
|
|
49896
|
+
validate: false,
|
|
49897
|
+
global
|
|
49898
|
+
});
|
|
49899
|
+
}
|
|
49900
|
+
};
|
|
49901
|
+
//#endregion
|
|
48912
49902
|
//#region src/features/skills/cursor-skill.ts
|
|
48913
49903
|
const CursorSkillFrontmatterSchema = z.looseObject({
|
|
48914
49904
|
name: z.string(),
|
|
@@ -49073,9 +50063,9 @@ const DeepagentsSkillFrontmatterSchema = z.looseObject({
|
|
|
49073
50063
|
name: z.string(),
|
|
49074
50064
|
description: z.string(),
|
|
49075
50065
|
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
49076
|
-
license: z.optional(z.
|
|
49077
|
-
compatibility: z.optional(z.
|
|
49078
|
-
metadata: z.optional(z.
|
|
50066
|
+
license: z.optional(z.unknown()),
|
|
50067
|
+
compatibility: z.optional(z.unknown()),
|
|
50068
|
+
metadata: z.optional(z.unknown())
|
|
49079
50069
|
});
|
|
49080
50070
|
var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
|
|
49081
50071
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DEEPAGENTS_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -50133,9 +51123,9 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
50133
51123
|
const KiloSkillFrontmatterSchema = z.looseObject({
|
|
50134
51124
|
name: z.string(),
|
|
50135
51125
|
description: z.string(),
|
|
50136
|
-
license: z.optional(z.
|
|
50137
|
-
compatibility: z.optional(z.
|
|
50138
|
-
metadata: z.optional(z.
|
|
51126
|
+
license: z.optional(z.unknown()),
|
|
51127
|
+
compatibility: z.optional(z.unknown()),
|
|
51128
|
+
metadata: z.optional(z.unknown()),
|
|
50139
51129
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
50140
51130
|
});
|
|
50141
51131
|
var KiloSkill = class KiloSkill extends ToolSkill {
|
|
@@ -50572,10 +51562,11 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
50572
51562
|
rootFrontmatter: rulesyncFrontmatter,
|
|
50573
51563
|
section: kiroSection
|
|
50574
51564
|
});
|
|
51565
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = kiroSection ?? {};
|
|
50575
51566
|
const kiroFrontmatter = {
|
|
50576
|
-
...kiroSection,
|
|
50577
51567
|
name: rulesyncFrontmatter.name,
|
|
50578
51568
|
description: rulesyncFrontmatter.description,
|
|
51569
|
+
...section,
|
|
50579
51570
|
...license !== void 0 && { license },
|
|
50580
51571
|
...compatibility !== void 0 && { compatibility },
|
|
50581
51572
|
...metadata !== void 0 && { metadata }
|
|
@@ -50807,9 +51798,9 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
|
50807
51798
|
const OpenCodeSkillFrontmatterSchema = z.looseObject({
|
|
50808
51799
|
name: z.string(),
|
|
50809
51800
|
description: z.string(),
|
|
50810
|
-
license: z.optional(z.
|
|
50811
|
-
compatibility: z.optional(z.
|
|
50812
|
-
metadata: z.optional(z.
|
|
51801
|
+
license: z.optional(z.unknown()),
|
|
51802
|
+
compatibility: z.optional(z.unknown()),
|
|
51803
|
+
metadata: z.optional(z.unknown()),
|
|
50813
51804
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
50814
51805
|
});
|
|
50815
51806
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
@@ -52723,6 +53714,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
52723
53714
|
supportsGlobal: true
|
|
52724
53715
|
}
|
|
52725
53716
|
}],
|
|
53717
|
+
["crush", {
|
|
53718
|
+
class: CrushSkill,
|
|
53719
|
+
meta: {
|
|
53720
|
+
supportsProject: true,
|
|
53721
|
+
supportsSimulated: false,
|
|
53722
|
+
supportsGlobal: true
|
|
53723
|
+
}
|
|
53724
|
+
}],
|
|
52726
53725
|
["cursor", {
|
|
52727
53726
|
class: CursorSkill,
|
|
52728
53727
|
meta: {
|
|
@@ -60466,6 +61465,238 @@ var ClineRule = class ClineRule extends ToolRule {
|
|
|
60466
61465
|
}
|
|
60467
61466
|
};
|
|
60468
61467
|
//#endregion
|
|
61468
|
+
//#region src/features/rules/codebuddy-rule.ts
|
|
61469
|
+
/**
|
|
61470
|
+
* Frontmatter schema for CodeBuddy Code modular rules.
|
|
61471
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61472
|
+
*/
|
|
61473
|
+
const CodebuddyRuleFrontmatterSchema = z.object({
|
|
61474
|
+
description: z.optional(z.string()),
|
|
61475
|
+
paths: z.optional(z.array(z.string())),
|
|
61476
|
+
alwaysApply: z.optional(z.boolean())
|
|
61477
|
+
});
|
|
61478
|
+
/**
|
|
61479
|
+
* A universal glob (matching everything) is redundant on an Always Apply
|
|
61480
|
+
* rule and, paired with `alwaysApply: true`, is the same semantic conflict
|
|
61481
|
+
* `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
|
|
61482
|
+
* applies the rule everywhere, so also emitting an explicit
|
|
61483
|
+
* `paths: ["**\/*"]` is at best redundant and, on a subsequent
|
|
61484
|
+
* import/generate round-trip, misleadingly implies the rule is scoped by
|
|
61485
|
+
* path rather than always-on.
|
|
61486
|
+
*/
|
|
61487
|
+
const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
61488
|
+
/**
|
|
61489
|
+
* Rule generator for CodeBuddy Code, Tencent Cloud's terminal coding agent
|
|
61490
|
+
* (`@tencent-ai/codebuddy-code`). Its configuration surface mirrors Claude
|
|
61491
|
+
* Code closely.
|
|
61492
|
+
*
|
|
61493
|
+
* Rules format:
|
|
61494
|
+
* - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
|
|
61495
|
+
* - {project}/.codebuddy/rules/*.md (root: false, with optional
|
|
61496
|
+
* `description` / `paths` / `alwaysApply` frontmatter)
|
|
61497
|
+
* - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
|
|
61498
|
+
*
|
|
61499
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61500
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
61501
|
+
*/
|
|
61502
|
+
var CodebuddyRule = class CodebuddyRule extends ToolRule {
|
|
61503
|
+
frontmatter;
|
|
61504
|
+
body;
|
|
61505
|
+
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61506
|
+
if (global) return {
|
|
61507
|
+
root: {
|
|
61508
|
+
relativeDirPath: buildToolPath(CODEBUDDY_DIR, ".", excludeToolDir),
|
|
61509
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61510
|
+
},
|
|
61511
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61512
|
+
};
|
|
61513
|
+
return {
|
|
61514
|
+
root: {
|
|
61515
|
+
relativeDirPath: ".",
|
|
61516
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61517
|
+
},
|
|
61518
|
+
alternativeRoots: [{
|
|
61519
|
+
relativeDirPath: CODEBUDDY_DIR,
|
|
61520
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61521
|
+
}],
|
|
61522
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61523
|
+
};
|
|
61524
|
+
}
|
|
61525
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
61526
|
+
if (rest.validate) {
|
|
61527
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61528
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
61529
|
+
}
|
|
61530
|
+
super({
|
|
61531
|
+
...rest,
|
|
61532
|
+
fileContent: rest.root ? body : CodebuddyRule.generateFileContent(body, frontmatter)
|
|
61533
|
+
});
|
|
61534
|
+
this.frontmatter = frontmatter;
|
|
61535
|
+
this.body = body;
|
|
61536
|
+
}
|
|
61537
|
+
static generateFileContent(body, frontmatter) {
|
|
61538
|
+
if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
|
|
61539
|
+
return stringifyFrontmatter(body, {
|
|
61540
|
+
description: frontmatter.description,
|
|
61541
|
+
alwaysApply: frontmatter.alwaysApply,
|
|
61542
|
+
paths: frontmatter.paths
|
|
61543
|
+
});
|
|
61544
|
+
}
|
|
61545
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, relativeDirPath: overrideDirPath }) {
|
|
61546
|
+
const paths = this.getSettablePaths({ global });
|
|
61547
|
+
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61548
|
+
const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
|
|
61549
|
+
const fileContent = await readFileContent(join(outputRoot, rootDirPath, paths.root.relativeFilePath));
|
|
61550
|
+
return new CodebuddyRule({
|
|
61551
|
+
outputRoot,
|
|
61552
|
+
relativeDirPath: rootDirPath,
|
|
61553
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61554
|
+
frontmatter: {},
|
|
61555
|
+
body: fileContent.trim(),
|
|
61556
|
+
validate,
|
|
61557
|
+
root: true
|
|
61558
|
+
});
|
|
61559
|
+
}
|
|
61560
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
|
|
61561
|
+
const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
61562
|
+
const filePath = join(outputRoot, relativePath);
|
|
61563
|
+
const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
61564
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61565
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
61566
|
+
return new CodebuddyRule({
|
|
61567
|
+
outputRoot,
|
|
61568
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61569
|
+
relativeFilePath,
|
|
61570
|
+
frontmatter: result.data,
|
|
61571
|
+
body: content.trim(),
|
|
61572
|
+
validate,
|
|
61573
|
+
root: false
|
|
61574
|
+
});
|
|
61575
|
+
}
|
|
61576
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61577
|
+
const isRoot = relativeFilePath === this.getSettablePaths({ global }).root.relativeFilePath;
|
|
61578
|
+
return new CodebuddyRule({
|
|
61579
|
+
outputRoot,
|
|
61580
|
+
relativeDirPath,
|
|
61581
|
+
relativeFilePath,
|
|
61582
|
+
frontmatter: {},
|
|
61583
|
+
body: "",
|
|
61584
|
+
validate: false,
|
|
61585
|
+
root: isRoot
|
|
61586
|
+
});
|
|
61587
|
+
}
|
|
61588
|
+
static resolveCodebuddyPaths({ paths, alwaysApply }) {
|
|
61589
|
+
if (!paths || paths.length === 0) return;
|
|
61590
|
+
if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
|
|
61591
|
+
return paths;
|
|
61592
|
+
}
|
|
61593
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
61594
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
61595
|
+
const root = rulesyncFrontmatter.root ?? false;
|
|
61596
|
+
const paths = this.getSettablePaths({ global });
|
|
61597
|
+
const body = rulesyncRule.getBody();
|
|
61598
|
+
if (root) return new CodebuddyRule({
|
|
61599
|
+
outputRoot,
|
|
61600
|
+
frontmatter: {},
|
|
61601
|
+
body,
|
|
61602
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
61603
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61604
|
+
validate,
|
|
61605
|
+
root
|
|
61606
|
+
});
|
|
61607
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
|
|
61608
|
+
const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
|
|
61609
|
+
const globs = rulesyncFrontmatter.globs;
|
|
61610
|
+
const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
|
|
61611
|
+
const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
|
|
61612
|
+
paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
|
|
61613
|
+
alwaysApply: alwaysApply === true
|
|
61614
|
+
});
|
|
61615
|
+
const codebuddyFrontmatter = {
|
|
61616
|
+
description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
|
|
61617
|
+
paths: pathsValue,
|
|
61618
|
+
alwaysApply
|
|
61619
|
+
};
|
|
61620
|
+
return new CodebuddyRule({
|
|
61621
|
+
outputRoot,
|
|
61622
|
+
frontmatter: codebuddyFrontmatter,
|
|
61623
|
+
body,
|
|
61624
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61625
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
61626
|
+
validate,
|
|
61627
|
+
root
|
|
61628
|
+
});
|
|
61629
|
+
}
|
|
61630
|
+
toRulesyncRule() {
|
|
61631
|
+
const targets = ["*"];
|
|
61632
|
+
if (this.isRoot()) {
|
|
61633
|
+
const rulesyncFrontmatter = {
|
|
61634
|
+
targets,
|
|
61635
|
+
root: true,
|
|
61636
|
+
description: this.description,
|
|
61637
|
+
globs: ["**/*"]
|
|
61638
|
+
};
|
|
61639
|
+
return new RulesyncRule({
|
|
61640
|
+
outputRoot: this.getOutputRoot(),
|
|
61641
|
+
frontmatter: rulesyncFrontmatter,
|
|
61642
|
+
body: this.body,
|
|
61643
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61644
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61645
|
+
validate: true
|
|
61646
|
+
});
|
|
61647
|
+
}
|
|
61648
|
+
const isAlways = this.frontmatter.alwaysApply === true;
|
|
61649
|
+
const sourcePaths = this.frontmatter.paths ?? [];
|
|
61650
|
+
const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
|
|
61651
|
+
const rulesyncFrontmatter = {
|
|
61652
|
+
targets,
|
|
61653
|
+
root: false,
|
|
61654
|
+
description: this.frontmatter.description,
|
|
61655
|
+
globs,
|
|
61656
|
+
...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
|
|
61657
|
+
paths: this.frontmatter.paths,
|
|
61658
|
+
alwaysApply: this.frontmatter.alwaysApply,
|
|
61659
|
+
description: this.frontmatter.description
|
|
61660
|
+
} }
|
|
61661
|
+
};
|
|
61662
|
+
return new RulesyncRule({
|
|
61663
|
+
outputRoot: this.getOutputRoot(),
|
|
61664
|
+
frontmatter: rulesyncFrontmatter,
|
|
61665
|
+
body: this.body,
|
|
61666
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61667
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61668
|
+
validate: true
|
|
61669
|
+
});
|
|
61670
|
+
}
|
|
61671
|
+
validate() {
|
|
61672
|
+
if (!this.frontmatter) return {
|
|
61673
|
+
success: true,
|
|
61674
|
+
error: null
|
|
61675
|
+
};
|
|
61676
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
61677
|
+
if (result.success) return {
|
|
61678
|
+
success: true,
|
|
61679
|
+
error: null
|
|
61680
|
+
};
|
|
61681
|
+
else return {
|
|
61682
|
+
success: false,
|
|
61683
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
61684
|
+
};
|
|
61685
|
+
}
|
|
61686
|
+
getFrontmatter() {
|
|
61687
|
+
return this.frontmatter;
|
|
61688
|
+
}
|
|
61689
|
+
getBody() {
|
|
61690
|
+
return this.body;
|
|
61691
|
+
}
|
|
61692
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
61693
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
61694
|
+
rulesyncRule,
|
|
61695
|
+
toolTarget: "codebuddy"
|
|
61696
|
+
});
|
|
61697
|
+
}
|
|
61698
|
+
};
|
|
61699
|
+
//#endregion
|
|
60469
61700
|
//#region src/features/rules/codexcli-rule.ts
|
|
60470
61701
|
var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
60471
61702
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -60762,6 +61993,79 @@ var CopilotcliRule = class CopilotcliRule extends CopilotRule {
|
|
|
60762
61993
|
}
|
|
60763
61994
|
};
|
|
60764
61995
|
//#endregion
|
|
61996
|
+
//#region src/features/rules/crush-rule.ts
|
|
61997
|
+
var CrushRule = class CrushRule extends ToolRule {
|
|
61998
|
+
constructor({ fileContent, root, ...rest }) {
|
|
61999
|
+
super({
|
|
62000
|
+
...rest,
|
|
62001
|
+
fileContent,
|
|
62002
|
+
root: root ?? false
|
|
62003
|
+
});
|
|
62004
|
+
}
|
|
62005
|
+
static getSettablePaths({ global = false } = {}) {
|
|
62006
|
+
if (global) return { root: {
|
|
62007
|
+
relativeDirPath: CRUSH_GLOBAL_DIR,
|
|
62008
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
62009
|
+
} };
|
|
62010
|
+
return { root: {
|
|
62011
|
+
relativeDirPath: ".",
|
|
62012
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
62013
|
+
} };
|
|
62014
|
+
}
|
|
62015
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
62016
|
+
const { root } = this.getSettablePaths({ global });
|
|
62017
|
+
const relativePath = join(root.relativeDirPath, root.relativeFilePath);
|
|
62018
|
+
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
62019
|
+
return new CrushRule({
|
|
62020
|
+
outputRoot,
|
|
62021
|
+
relativeDirPath: root.relativeDirPath,
|
|
62022
|
+
relativeFilePath: root.relativeFilePath,
|
|
62023
|
+
fileContent,
|
|
62024
|
+
validate,
|
|
62025
|
+
root: true
|
|
62026
|
+
});
|
|
62027
|
+
}
|
|
62028
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
62029
|
+
const { root } = this.getSettablePaths({ global });
|
|
62030
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
62031
|
+
return new CrushRule({
|
|
62032
|
+
outputRoot,
|
|
62033
|
+
relativeDirPath: root.relativeDirPath,
|
|
62034
|
+
relativeFilePath: root.relativeFilePath,
|
|
62035
|
+
fileContent: rulesyncRule.getBody(),
|
|
62036
|
+
validate,
|
|
62037
|
+
root: isRoot
|
|
62038
|
+
});
|
|
62039
|
+
}
|
|
62040
|
+
toRulesyncRule() {
|
|
62041
|
+
return this.toRulesyncRuleDefault();
|
|
62042
|
+
}
|
|
62043
|
+
validate() {
|
|
62044
|
+
return {
|
|
62045
|
+
success: true,
|
|
62046
|
+
error: null
|
|
62047
|
+
};
|
|
62048
|
+
}
|
|
62049
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
62050
|
+
const { root } = this.getSettablePaths({ global });
|
|
62051
|
+
const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
|
|
62052
|
+
return new CrushRule({
|
|
62053
|
+
outputRoot,
|
|
62054
|
+
relativeDirPath,
|
|
62055
|
+
relativeFilePath,
|
|
62056
|
+
fileContent: "",
|
|
62057
|
+
validate: false,
|
|
62058
|
+
root: isRoot
|
|
62059
|
+
});
|
|
62060
|
+
}
|
|
62061
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
62062
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
62063
|
+
rulesyncRule,
|
|
62064
|
+
toolTarget: "crush"
|
|
62065
|
+
});
|
|
62066
|
+
}
|
|
62067
|
+
};
|
|
62068
|
+
//#endregion
|
|
60765
62069
|
//#region src/features/rules/cursor-rule.ts
|
|
60766
62070
|
const CursorRuleFrontmatterSchema = z.object({
|
|
60767
62071
|
description: z.optional(z.string()),
|
|
@@ -61369,13 +62673,34 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
61369
62673
|
};
|
|
61370
62674
|
//#endregion
|
|
61371
62675
|
//#region src/features/rules/factorydroid-rule.ts
|
|
62676
|
+
/**
|
|
62677
|
+
* Rule generator for Factory Droid.
|
|
62678
|
+
*
|
|
62679
|
+
* Factory Droid loads the root `AGENTS.md` (project) / `~/.factory/AGENTS.md`
|
|
62680
|
+
* (global) as coding guidelines, plus non-root rules referenced from it via
|
|
62681
|
+
* `.factory/rules/*.md`.
|
|
62682
|
+
*
|
|
62683
|
+
* Factory Droid also loads `DESIGN.md` (project only) as a second,
|
|
62684
|
+
* independent instruction surface: "Always-on design-system, UX, visual, and
|
|
62685
|
+
* interaction guidance", loaded separately from `AGENTS.md`'s coding
|
|
62686
|
+
* guidelines. Rulesync emits it from any non-root rule that opts in via a
|
|
62687
|
+
* `factorydroid.channel: design` frontmatter block — those rule bodies are
|
|
62688
|
+
* routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
|
|
62689
|
+
* multiple opted-in rules concatenate in source order. Factory's docs describe
|
|
62690
|
+
* `DESIGN.md` at the repository root and in nested subdirectories, like
|
|
62691
|
+
* `AGENTS.md`, but document no personal/global home-directory equivalent, so
|
|
62692
|
+
* this channel is project scope only.
|
|
62693
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
62694
|
+
*/
|
|
61372
62695
|
var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
61373
|
-
|
|
62696
|
+
design;
|
|
62697
|
+
constructor({ fileContent, root, design = false, ...rest }) {
|
|
61374
62698
|
super({
|
|
61375
62699
|
...rest,
|
|
61376
62700
|
fileContent,
|
|
61377
62701
|
root: root ?? false
|
|
61378
62702
|
});
|
|
62703
|
+
this.design = design;
|
|
61379
62704
|
}
|
|
61380
62705
|
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61381
62706
|
if (global) return { root: {
|
|
@@ -61387,11 +62712,47 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61387
62712
|
relativeDirPath: ".",
|
|
61388
62713
|
relativeFilePath: FACTORYDROID_RULE_FILE_NAME
|
|
61389
62714
|
},
|
|
61390
|
-
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) }
|
|
62715
|
+
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) },
|
|
62716
|
+
design: {
|
|
62717
|
+
relativeDirPath: ".",
|
|
62718
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
|
|
62719
|
+
}
|
|
61391
62720
|
};
|
|
61392
62721
|
}
|
|
61393
|
-
|
|
62722
|
+
/**
|
|
62723
|
+
* Extra fixed files this tool manages beyond the root/non-root rules. The
|
|
62724
|
+
* RulesProcessor enumerates these for import and deletion so a stale
|
|
62725
|
+
* `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
|
|
62726
|
+
* mode: `DESIGN.md` has no documented home-directory equivalent.
|
|
62727
|
+
*/
|
|
62728
|
+
static getExtraFixedFiles({ global = false } = {}) {
|
|
62729
|
+
if (global) return [];
|
|
62730
|
+
return [this.getSettablePaths({ global }).design];
|
|
62731
|
+
}
|
|
62732
|
+
/**
|
|
62733
|
+
* Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
|
|
62734
|
+
* TOON reference section would double-load the content (and misrepresent it
|
|
62735
|
+
* as a rule the model must remember to open).
|
|
62736
|
+
*/
|
|
62737
|
+
isExcludedFromRootReferences() {
|
|
62738
|
+
return this.design;
|
|
62739
|
+
}
|
|
62740
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
61394
62741
|
const paths = this.getSettablePaths({ global });
|
|
62742
|
+
const design = !global ? paths.design : void 0;
|
|
62743
|
+
if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
|
|
62744
|
+
const relativePath = join(design.relativeDirPath, design.relativeFilePath);
|
|
62745
|
+
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
62746
|
+
return new FactorydroidRule({
|
|
62747
|
+
outputRoot,
|
|
62748
|
+
relativeDirPath: design.relativeDirPath,
|
|
62749
|
+
relativeFilePath: design.relativeFilePath,
|
|
62750
|
+
fileContent,
|
|
62751
|
+
validate,
|
|
62752
|
+
root: false,
|
|
62753
|
+
design: true
|
|
62754
|
+
});
|
|
62755
|
+
}
|
|
61395
62756
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61396
62757
|
const relativePath = join(paths.root.relativeDirPath, paths.root.relativeFilePath);
|
|
61397
62758
|
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
@@ -61418,18 +62779,34 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61418
62779
|
}
|
|
61419
62780
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61420
62781
|
const paths = this.getSettablePaths({ global });
|
|
61421
|
-
const
|
|
62782
|
+
const design = !global ? paths.design : void 0;
|
|
62783
|
+
const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
|
|
62784
|
+
const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
61422
62785
|
return new FactorydroidRule({
|
|
61423
62786
|
outputRoot,
|
|
61424
62787
|
relativeDirPath,
|
|
61425
62788
|
relativeFilePath,
|
|
61426
62789
|
fileContent: "",
|
|
61427
62790
|
validate: false,
|
|
61428
|
-
root: isRoot
|
|
62791
|
+
root: isRoot,
|
|
62792
|
+
design: isDesign
|
|
61429
62793
|
});
|
|
61430
62794
|
}
|
|
61431
62795
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
62796
|
+
const frontmatter = rulesyncRule.getFrontmatter();
|
|
61432
62797
|
const paths = this.getSettablePaths({ global });
|
|
62798
|
+
if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
|
|
62799
|
+
const { design } = paths;
|
|
62800
|
+
return new FactorydroidRule({
|
|
62801
|
+
outputRoot,
|
|
62802
|
+
relativeDirPath: design.relativeDirPath,
|
|
62803
|
+
relativeFilePath: design.relativeFilePath,
|
|
62804
|
+
fileContent: rulesyncRule.getBody(),
|
|
62805
|
+
validate,
|
|
62806
|
+
root: false,
|
|
62807
|
+
design: true
|
|
62808
|
+
});
|
|
62809
|
+
}
|
|
61433
62810
|
return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
|
|
61434
62811
|
outputRoot,
|
|
61435
62812
|
rulesyncRule,
|
|
@@ -61439,6 +62816,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61439
62816
|
}));
|
|
61440
62817
|
}
|
|
61441
62818
|
toRulesyncRule() {
|
|
62819
|
+
if (this.design) return new RulesyncRule({
|
|
62820
|
+
outputRoot: process.cwd(),
|
|
62821
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
62822
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
|
|
62823
|
+
frontmatter: {
|
|
62824
|
+
root: false,
|
|
62825
|
+
targets: ["factorydroid"],
|
|
62826
|
+
factorydroid: { channel: "design" }
|
|
62827
|
+
},
|
|
62828
|
+
body: this.getFileContent()
|
|
62829
|
+
});
|
|
61442
62830
|
return this.toRulesyncRuleDefault();
|
|
61443
62831
|
}
|
|
61444
62832
|
validate() {
|
|
@@ -63956,6 +65344,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
63956
65344
|
ruleDiscoveryMode: "auto"
|
|
63957
65345
|
}
|
|
63958
65346
|
}],
|
|
65347
|
+
["codebuddy", {
|
|
65348
|
+
class: CodebuddyRule,
|
|
65349
|
+
meta: {
|
|
65350
|
+
extension: "md",
|
|
65351
|
+
supportsGlobal: true,
|
|
65352
|
+
ruleDiscoveryMode: "auto",
|
|
65353
|
+
localRootMode: "separate-local-file",
|
|
65354
|
+
localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
|
|
65355
|
+
}
|
|
65356
|
+
}],
|
|
63959
65357
|
["codexcli", {
|
|
63960
65358
|
class: CodexcliRule,
|
|
63961
65359
|
meta: {
|
|
@@ -63981,6 +65379,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
63981
65379
|
ruleDiscoveryMode: "auto"
|
|
63982
65380
|
}
|
|
63983
65381
|
}],
|
|
65382
|
+
["crush", {
|
|
65383
|
+
class: CrushRule,
|
|
65384
|
+
meta: {
|
|
65385
|
+
extension: "md",
|
|
65386
|
+
supportsGlobal: true,
|
|
65387
|
+
ruleDiscoveryMode: "auto",
|
|
65388
|
+
collisionPolicy: "fold"
|
|
65389
|
+
}
|
|
65390
|
+
}],
|
|
63984
65391
|
["cursor", {
|
|
63985
65392
|
class: CursorRule,
|
|
63986
65393
|
meta: {
|
|
@@ -64349,6 +65756,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64349
65756
|
outputFiles,
|
|
64350
65757
|
convertedRules
|
|
64351
65758
|
});
|
|
65759
|
+
await this.warnForDeactivatedImportOnlyRoots({
|
|
65760
|
+
toolRules,
|
|
65761
|
+
factory
|
|
65762
|
+
});
|
|
64352
65763
|
return outputFiles;
|
|
64353
65764
|
}
|
|
64354
65765
|
/**
|
|
@@ -64570,6 +65981,41 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64570
65981
|
}
|
|
64571
65982
|
}
|
|
64572
65983
|
/**
|
|
65984
|
+
* Warn when this generate run is about to write a root rule file that will
|
|
65985
|
+
* make the tool stop reading paths it currently reads instead — Junie's
|
|
65986
|
+
* `.junie/rules/*.md` and `.junie/playbook.md` become unreachable the
|
|
65987
|
+
* moment `.junie/AGENTS.md` exists, since Junie reads the root file
|
|
65988
|
+
* exclusively once it is present. `importOnlyRoots` with
|
|
65989
|
+
* `onlyWhenRootAbsent` already models exactly this shape for import; this
|
|
65990
|
+
* reuses the same declaration so the
|
|
65991
|
+
* `generate` path — which never calls `loadToolFiles` and so never reached
|
|
65992
|
+
* the existing import-side warning — surfaces it too. Without this, a repo
|
|
65993
|
+
* that only ever runs `generate` never sees any warning: the deactivated
|
|
65994
|
+
* files stay on disk, untouched and not gitignored, silently unread.
|
|
65995
|
+
*/
|
|
65996
|
+
async warnForDeactivatedImportOnlyRoots({ toolRules, factory }) {
|
|
65997
|
+
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
65998
|
+
if (!rootRule) return;
|
|
65999
|
+
const settablePaths = factory.class.getSettablePaths({ global: this.global });
|
|
66000
|
+
const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
|
|
66001
|
+
if (!importOnlyRoots || importOnlyRoots.length === 0) return;
|
|
66002
|
+
const existingPaths = [];
|
|
66003
|
+
for (const importOnlyRoot of importOnlyRoots) {
|
|
66004
|
+
if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
|
|
66005
|
+
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
66006
|
+
cwd: this.outputRoot,
|
|
66007
|
+
type: "file"
|
|
66008
|
+
});
|
|
66009
|
+
existingPaths.push(...matchedPaths);
|
|
66010
|
+
}
|
|
66011
|
+
if (existingPaths.length === 0) return;
|
|
66012
|
+
const rootFileRelativePath = join(rootRule.getRelativeDirPath(), rootRule.getRelativeFilePath());
|
|
66013
|
+
const names = existingPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
|
|
66014
|
+
const listedNames = names.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
66015
|
+
const remainingCount = names.length - listedNames.length;
|
|
66016
|
+
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.`);
|
|
66017
|
+
}
|
|
66018
|
+
/**
|
|
64573
66019
|
* Handle localRoot rule generation based on tool target.
|
|
64574
66020
|
* - `separate-local-file`: writes a dedicated `*.local.md` root file
|
|
64575
66021
|
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
|
|
@@ -64821,6 +66267,26 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64821
66267
|
}));
|
|
64822
66268
|
}
|
|
64823
66269
|
/**
|
|
66270
|
+
* Load and merge rulesync rule files from every configured input root's
|
|
66271
|
+
* `.rulesync/rules/` directory, by relative path, so that a rule with the
|
|
66272
|
+
* same target path from a later root replaces the earlier root's copy
|
|
66273
|
+
* (case-insensitive, matching the intra-root collision handling).
|
|
66274
|
+
*
|
|
66275
|
+
* This is the side-effect-free half of `loadRulesyncFiles`: it does not
|
|
66276
|
+
* warn about a missing root rule or validate `localRoot` placement, so it
|
|
66277
|
+
* is also safe to call from code paths — like
|
|
66278
|
+
* `warnForFoldImportDuplicationRisk` — that only need the merged rule set
|
|
66279
|
+
* and must not trigger `loadRulesyncFiles`'s generate-time checks.
|
|
66280
|
+
*/
|
|
66281
|
+
async loadMergedRulesyncRules() {
|
|
66282
|
+
return mergeByCaseInsensitiveIdentity({
|
|
66283
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
66284
|
+
identity: (rule) => rule.getRelativeFilePath(),
|
|
66285
|
+
artifactName: "rule",
|
|
66286
|
+
logger: this.logger
|
|
66287
|
+
});
|
|
66288
|
+
}
|
|
66289
|
+
/**
|
|
64824
66290
|
* Implementation of abstract method from FeatureProcessor
|
|
64825
66291
|
* Load and parse rulesync rule files from every configured input root's
|
|
64826
66292
|
* `.rulesync/rules/` directory, merging by relative path so that a rule
|
|
@@ -64828,12 +66294,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64828
66294
|
* copy (case-insensitive, matching the intra-root collision handling).
|
|
64829
66295
|
*/
|
|
64830
66296
|
async loadRulesyncFiles() {
|
|
64831
|
-
const rulesyncRules =
|
|
64832
|
-
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
64833
|
-
identity: (rule) => rule.getRelativeFilePath(),
|
|
64834
|
-
artifactName: "rule",
|
|
64835
|
-
logger: this.logger
|
|
64836
|
-
});
|
|
66297
|
+
const rulesyncRules = await this.loadMergedRulesyncRules();
|
|
64837
66298
|
const factory = this.getFactory(this.toolTarget);
|
|
64838
66299
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
64839
66300
|
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}.`);
|
|
@@ -64890,6 +66351,46 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64890
66351
|
});
|
|
64891
66352
|
}
|
|
64892
66353
|
/**
|
|
66354
|
+
* Warn when importing a `collisionPolicy: "fold"` target's root file while
|
|
66355
|
+
* `.rulesync/rules/` still holds non-root rules targeting it. A fold target
|
|
66356
|
+
* (codexcli, junie, and others) concatenates every targeted non-root rule
|
|
66357
|
+
* into its one root output file on `generate`. Importing that root file
|
|
66358
|
+
* back therefore re-reads the already-folded content as a single new
|
|
66359
|
+
* rulesync rule, while the original non-root rules stay in place
|
|
66360
|
+
* untouched — the next `generate` folds both together, duplicating the
|
|
66361
|
+
* content once per generate/import cycle with nothing to indicate why.
|
|
66362
|
+
*
|
|
66363
|
+
* This does not attempt to detect or drop the specific duplicated content
|
|
66364
|
+
* (the root file has no marker recording which rule contributed what); it
|
|
66365
|
+
* only surfaces that the cycle produces one, per the "at minimum, warn"
|
|
66366
|
+
* option recorded on issue #2743.
|
|
66367
|
+
*
|
|
66368
|
+
* Only the actual `rulesync import` call site invokes this (and only once
|
|
66369
|
+
* it has confirmed there is something to import) — `loadToolFiles` is also
|
|
66370
|
+
* the entry point for `rulesync convert` and `rulesync fetch`, neither of
|
|
66371
|
+
* which writes to `.rulesync/rules/` or carries this duplication risk.
|
|
66372
|
+
*
|
|
66373
|
+
* Reads via `loadMergedRulesyncRules` rather than `loadRulesyncFiles`
|
|
66374
|
+
* deliberately: this runs before the imported root file is written, so
|
|
66375
|
+
* `.rulesync/rules/` never yet has a root rule targeting this tool, and
|
|
66376
|
+
* `loadRulesyncFiles`'s "no root rule found" warning and `localRoot`
|
|
66377
|
+
* validation (which can throw) would fire spuriously on every fold-tool
|
|
66378
|
+
* import — including ones where nothing is actually misconfigured.
|
|
66379
|
+
*
|
|
66380
|
+
* In global mode, a `localRoot: true` rule is excluded from the
|
|
66381
|
+
* duplication check the same way `loadRulesyncFiles`'s global-mode branch
|
|
66382
|
+
* excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
|
|
66383
|
+
* in global mode, so such a rule is never actually folded into the global
|
|
66384
|
+
* root output and warning about it here would be inaccurate.
|
|
66385
|
+
*/
|
|
66386
|
+
async warnForFoldImportDuplicationRisk() {
|
|
66387
|
+
const factory = this.getFactory(this.toolTarget);
|
|
66388
|
+
if (factory.meta.collisionPolicy !== "fold") return;
|
|
66389
|
+
const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
|
|
66390
|
+
if (nonRootRules.length === 0) return;
|
|
66391
|
+
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.`);
|
|
66392
|
+
}
|
|
66393
|
+
/**
|
|
64893
66394
|
* Implementation of abstract method from FeatureProcessor
|
|
64894
66395
|
* Load tool-specific rule configurations and parse them into ToolRule instances
|
|
64895
66396
|
*/
|
|
@@ -66953,6 +68454,7 @@ async function importRulesCore(params) {
|
|
|
66953
68454
|
logger.warn(`No rule files found for ${tool}. Skipping import.`);
|
|
66954
68455
|
return 0;
|
|
66955
68456
|
}
|
|
68457
|
+
await rulesProcessor.warnForFoldImportDuplicationRisk();
|
|
66956
68458
|
const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
66957
68459
|
const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
|
|
66958
68460
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);
|
|
@@ -67200,6 +68702,6 @@ async function importChecksCore(params) {
|
|
|
67200
68702
|
return writtenCount;
|
|
67201
68703
|
}
|
|
67202
68704
|
//#endregion
|
|
67203
|
-
export {
|
|
68705
|
+
export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
|
|
67204
68706
|
|
|
67205
|
-
//# sourceMappingURL=import-
|
|
68707
|
+
//# sourceMappingURL=import-DUE1P1zV.js.map
|