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
|
@@ -246,6 +246,35 @@ function hasDeceptiveHiddenCharacters(text) {
|
|
|
246
246
|
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
|
+
/** A mark that draws around the character before it — a circle, a square, a keycap box. */
|
|
250
|
+
const ENCLOSING_MARK_PATTERN = /\p{Me}/u;
|
|
251
|
+
/**
|
|
252
|
+
* Whether `text` carries an enclosing mark that is not the keycap of an emoji
|
|
253
|
+
* keycap sequence.
|
|
254
|
+
*
|
|
255
|
+
* An enclosing mark (`\p{Me}`: U+20DD COMBINING ENCLOSING CIRCLE, U+20E3
|
|
256
|
+
* COMBINING ENCLOSING KEYCAP, the Cyrillic and Vedic ones) is drawn over the
|
|
257
|
+
* character before it and takes no column of its own, so `pdf` with one after
|
|
258
|
+
* it occupies the three columns of `pdf` and is a fourth directory underneath.
|
|
259
|
+
* Unlike a joiner or a variation selector it is not invisible — the box is
|
|
260
|
+
* drawn — which is why `hasDeceptiveHiddenCharacters` does not refuse it and
|
|
261
|
+
* why it is a question for the confusable-name note instead: the row is drawn,
|
|
262
|
+
* only not the way its name reads. The one place an enclosing mark belongs in
|
|
263
|
+
* a name is the keycap sequence of UTS #51, which `isKeycapSequence` matches
|
|
264
|
+
* whole; every other one is left over.
|
|
265
|
+
*
|
|
266
|
+
* Restricted to `\p{Me}` on purpose: a non-spacing mark (`\p{Mn}`) is how
|
|
267
|
+
* Devanagari, Arabic and Vietnamese write, and folding those would mark
|
|
268
|
+
* ordinary names in every one of them.
|
|
269
|
+
*/
|
|
270
|
+
function hasEnclosingMarkOutsideKeycap(text) {
|
|
271
|
+
const characters = [...text];
|
|
272
|
+
return characters.some((character, index) => ENCLOSING_MARK_PATTERN.test(character) && !isKeycapSequence({
|
|
273
|
+
base: characters[index - 2],
|
|
274
|
+
selector: characters[index - 1] ?? "",
|
|
275
|
+
following: character
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
249
278
|
//#endregion
|
|
250
279
|
//#region src/utils/truncate.ts
|
|
251
280
|
/**
|
|
@@ -415,34 +444,34 @@ const isFeatureValueEnabled = (value) => {
|
|
|
415
444
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
416
445
|
//#endregion
|
|
417
446
|
//#region src/constants/rulesync-paths.ts
|
|
418
|
-
const { join: join$
|
|
447
|
+
const { join: join$304 } = node_path.posix;
|
|
419
448
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
420
449
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
421
450
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
422
451
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
423
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
452
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$304(RULES_FEATURE_SUBDIR, ".curated");
|
|
424
453
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
425
454
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
426
455
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
427
456
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
428
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
429
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
430
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
431
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
432
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
433
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
434
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
435
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
436
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
437
|
-
join$
|
|
438
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
439
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
457
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$304(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
458
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
459
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
460
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
461
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
462
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
463
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
464
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
465
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
466
|
+
join$304(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
467
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
468
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
440
469
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
441
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
470
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
442
471
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
443
472
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
444
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
445
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
473
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
446
475
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
447
476
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
448
477
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -469,9 +498,11 @@ const rulesProcessorToolTargetTuple = [
|
|
|
469
498
|
"claudecode",
|
|
470
499
|
"claudecode-legacy",
|
|
471
500
|
"cline",
|
|
501
|
+
"codebuddy",
|
|
472
502
|
"codexcli",
|
|
473
503
|
"copilot",
|
|
474
504
|
"copilotcli",
|
|
505
|
+
"crush",
|
|
475
506
|
"cursor",
|
|
476
507
|
"deepagents",
|
|
477
508
|
"factorydroid",
|
|
@@ -507,6 +538,7 @@ const ignoreProcessorToolTargetTuple = [
|
|
|
507
538
|
"claudecode",
|
|
508
539
|
"claudecode-legacy",
|
|
509
540
|
"cline",
|
|
541
|
+
"crush",
|
|
510
542
|
"cursor",
|
|
511
543
|
"hermesagent",
|
|
512
544
|
"junie",
|
|
@@ -648,6 +680,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
648
680
|
"codexcli",
|
|
649
681
|
"copilot",
|
|
650
682
|
"copilotcli",
|
|
683
|
+
"crush",
|
|
651
684
|
"cursor",
|
|
652
685
|
"deepagents",
|
|
653
686
|
"factorydroid",
|
|
@@ -3352,6 +3385,83 @@ var RulesyncFile = class extends AiFile {
|
|
|
3352
3385
|
}
|
|
3353
3386
|
};
|
|
3354
3387
|
//#endregion
|
|
3388
|
+
//#region src/utils/bounded-walk.ts
|
|
3389
|
+
const ALIAS_HINT = "(a chain of YAML aliases may be amplifying the document)";
|
|
3390
|
+
/**
|
|
3391
|
+
* Create the bookkeeping for one walk. `subject` names the document kind in
|
|
3392
|
+
* every error ("Frontmatter", "Shared config"); `root`, when given, is entered
|
|
3393
|
+
* up front so the root object counts as the first nesting level, matching the
|
|
3394
|
+
* +1 that `enter` applies to every container nested inside it.
|
|
3395
|
+
*/
|
|
3396
|
+
function createBoundedWalk({ subject, limits, root }) {
|
|
3397
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
3398
|
+
let valuesRemaining = limits.maxValues;
|
|
3399
|
+
let stringCharsRemaining = limits.maxStringChars;
|
|
3400
|
+
let depth = 0;
|
|
3401
|
+
const chargeChars = (chars) => {
|
|
3402
|
+
stringCharsRemaining -= chars;
|
|
3403
|
+
if (stringCharsRemaining < 0) throw new Error(`${subject}'s string values expand to more than ${limits.maxStringChars} characters; refusing to process it ${ALIAS_HINT}`);
|
|
3404
|
+
};
|
|
3405
|
+
const chargeValue = (stringChars = 0) => {
|
|
3406
|
+
valuesRemaining -= 1;
|
|
3407
|
+
if (valuesRemaining < 0) throw new Error(`${subject} expands to more than ${limits.maxValues} values; refusing to process it ${ALIAS_HINT}`);
|
|
3408
|
+
chargeChars(stringChars);
|
|
3409
|
+
};
|
|
3410
|
+
const enter = (container) => {
|
|
3411
|
+
depth += 1;
|
|
3412
|
+
if (depth > limits.maxDepth) throw new Error(`${subject} nests more than ${limits.maxDepth} levels deep; refusing to process it ${ALIAS_HINT}`);
|
|
3413
|
+
ancestors.add(container);
|
|
3414
|
+
};
|
|
3415
|
+
const leave = (container) => {
|
|
3416
|
+
ancestors.delete(container);
|
|
3417
|
+
depth -= 1;
|
|
3418
|
+
};
|
|
3419
|
+
if (root !== void 0) enter(root);
|
|
3420
|
+
return {
|
|
3421
|
+
chargeValue,
|
|
3422
|
+
chargeChars,
|
|
3423
|
+
isAncestor: (container) => ancestors.has(container),
|
|
3424
|
+
enter,
|
|
3425
|
+
leave
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
//#endregion
|
|
3429
|
+
//#region src/utils/prototype-pollution.ts
|
|
3430
|
+
/**
|
|
3431
|
+
* Keys that, if walked into when constructing or merging objects from
|
|
3432
|
+
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
3433
|
+
* chain) and propagate state to every other object in the runtime. Any code
|
|
3434
|
+
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
3435
|
+
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
3436
|
+
*/
|
|
3437
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
3438
|
+
"__proto__",
|
|
3439
|
+
"constructor",
|
|
3440
|
+
"prototype"
|
|
3441
|
+
]);
|
|
3442
|
+
function isPrototypePollutionKey(key) {
|
|
3443
|
+
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Returns a shallow copy of a record's own entries with every
|
|
3447
|
+
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
3448
|
+
*
|
|
3449
|
+
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
3450
|
+
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
3451
|
+
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
3452
|
+
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
3453
|
+
* prototype). Walking the entries through this helper severs that path while
|
|
3454
|
+
* preserving every legitimate key.
|
|
3455
|
+
*/
|
|
3456
|
+
function omitPrototypePollutionKeys(record) {
|
|
3457
|
+
const sanitized = {};
|
|
3458
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3459
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3460
|
+
sanitized[key] = value;
|
|
3461
|
+
}
|
|
3462
|
+
return sanitized;
|
|
3463
|
+
}
|
|
3464
|
+
//#endregion
|
|
3355
3465
|
//#region src/utils/type-guards.ts
|
|
3356
3466
|
/**
|
|
3357
3467
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
@@ -3416,67 +3526,154 @@ function loadYaml(content) {
|
|
|
3416
3526
|
}
|
|
3417
3527
|
//#endregion
|
|
3418
3528
|
//#region src/utils/frontmatter.ts
|
|
3419
|
-
|
|
3529
|
+
/**
|
|
3530
|
+
* Upper bound on the number of values a frontmatter document may expand to
|
|
3531
|
+
* once every YAML alias is written out.
|
|
3532
|
+
*
|
|
3533
|
+
* A YAML alias makes one parsed container reachable from many keys, and the
|
|
3534
|
+
* cleaners below copy each reachable value, so a small file with a few levels
|
|
3535
|
+
* of nested aliases (an "alias bomb") can expand into megabytes of output or
|
|
3536
|
+
* exhaust the heap. Counting every visited value against this budget turns
|
|
3537
|
+
* that into an error instead. Real frontmatter is a handful of keys; even a
|
|
3538
|
+
* generous skill manifest stays orders of magnitude below the limit.
|
|
3539
|
+
*/
|
|
3540
|
+
const MAX_FRONTMATTER_VALUES = 1e5;
|
|
3541
|
+
/**
|
|
3542
|
+
* Upper bound on the total character count of string leaves a frontmatter
|
|
3543
|
+
* document may expand to.
|
|
3544
|
+
*
|
|
3545
|
+
* {@link MAX_FRONTMATTER_VALUES} bounds how many values are visited, but a
|
|
3546
|
+
* single long string aliased thousands of times still fits that budget while
|
|
3547
|
+
* the duplicated output balloons: one scalar of a few KB, chained through a
|
|
3548
|
+
* handful of aliases within the value budget, can multiply into a document
|
|
3549
|
+
* many megabytes larger than it started. Charging every visited string's
|
|
3550
|
+
* length against this separate budget bounds that output regardless of how
|
|
3551
|
+
* many aliases point at it.
|
|
3552
|
+
*/
|
|
3553
|
+
const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
3554
|
+
/**
|
|
3555
|
+
* Upper bound on the raw character length of the `---`-delimited frontmatter
|
|
3556
|
+
* block itself, checked before it is ever handed to the YAML parser.
|
|
3557
|
+
*
|
|
3558
|
+
* The budgets above only bound the *parsed* document — the walk over
|
|
3559
|
+
* `matter()`'s output — but a complex YAML key (an array or mapping used as a
|
|
3560
|
+
* mapping key) is joined into a string by js-yaml while it parses, and a
|
|
3561
|
+
* mapping with many such keys can cost real memory before that walk ever
|
|
3562
|
+
* starts, or even before `matter()` returns. Capping the raw block size keeps
|
|
3563
|
+
* that parse-time cost bounded regardless of what the block contains. Real
|
|
3564
|
+
* frontmatter blocks are a few hundred bytes at most; even a large project
|
|
3565
|
+
* manifest stays well under this.
|
|
3566
|
+
*/
|
|
3567
|
+
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3568
|
+
/**
|
|
3569
|
+
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3570
|
+
* string leaf is charged by its own length instead).
|
|
3571
|
+
*
|
|
3572
|
+
* js-yaml's default schema resolves `!!binary` scalars to a `Uint8Array`, and
|
|
3573
|
+
* its dumper writes one back out as base64 — roughly 4 output characters per
|
|
3574
|
+
* 3 input bytes. Without this, an aliased binary blob would walk the budget
|
|
3575
|
+
* for free even though it can dominate the emitted document's size.
|
|
3576
|
+
*/
|
|
3577
|
+
function estimateLeafChars(value) {
|
|
3578
|
+
if (value instanceof Uint8Array) return Math.ceil(value.byteLength / 3) * 4;
|
|
3579
|
+
return 0;
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Copy one parsed value, dropping nullish leaves and cyclic references.
|
|
3583
|
+
*
|
|
3584
|
+
* Every alias is still written out as an independent copy, as gray-matter's
|
|
3585
|
+
* default YAML engine would otherwise serialize shared references as `&ref_0`
|
|
3586
|
+
* anchors that simplified frontmatter parsers cannot read; the expansion is
|
|
3587
|
+
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3588
|
+
*/
|
|
3589
|
+
function deepCleanValue(value, options) {
|
|
3590
|
+
const leafChars = typeof value === "string" ? value.length : estimateLeafChars(value);
|
|
3591
|
+
options.walk.chargeValue(leafChars);
|
|
3420
3592
|
if (value === null || value === void 0) return;
|
|
3421
|
-
if (
|
|
3593
|
+
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3594
|
+
if (Array.isArray(value)) {
|
|
3595
|
+
if (options.walk.isAncestor(value)) return;
|
|
3596
|
+
options.walk.enter(value);
|
|
3597
|
+
const cleanedArray = [];
|
|
3598
|
+
for (const item of value) {
|
|
3599
|
+
const cleaned = deepCleanValue(item, options);
|
|
3600
|
+
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3601
|
+
}
|
|
3602
|
+
options.walk.leave(value);
|
|
3603
|
+
return cleanedArray;
|
|
3604
|
+
}
|
|
3422
3605
|
if (isPlainObject$1(value)) {
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
}
|
|
3606
|
+
if (options.walk.isAncestor(value)) return;
|
|
3607
|
+
options.walk.enter(value);
|
|
3608
|
+
const result = cleanOwnEntries(value, options);
|
|
3609
|
+
options.walk.leave(value);
|
|
3428
3610
|
return result;
|
|
3429
3611
|
}
|
|
3430
3612
|
return value;
|
|
3431
3613
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3614
|
+
/**
|
|
3615
|
+
* Copy the cleaned own entries of a parsed object into a fresh record.
|
|
3616
|
+
*
|
|
3617
|
+
* A YAML parser defines a `__proto__:` key as an own property, and assigning
|
|
3618
|
+
* it back with bracket notation would instead replace the new record's
|
|
3619
|
+
* prototype, whose members zod's loose object schemas then promote to real
|
|
3620
|
+
* keys. So a fetched skill could hide `allowed-tools` under an innocuous
|
|
3621
|
+
* looking `__proto__:` block. That key, `constructor` and `prototype` are
|
|
3622
|
+
* therefore dropped rather than copied, and cannot be used as frontmatter
|
|
3623
|
+
* keys.
|
|
3624
|
+
*/
|
|
3625
|
+
function cleanOwnEntries(obj, options) {
|
|
3434
3626
|
const result = {};
|
|
3435
3627
|
for (const [key, val] of Object.entries(obj)) {
|
|
3436
|
-
|
|
3628
|
+
options.walk.chargeChars(key.length);
|
|
3629
|
+
const cleaned = deepCleanValue(val, options);
|
|
3630
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
3437
3631
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
3438
3632
|
}
|
|
3439
3633
|
return result;
|
|
3440
3634
|
}
|
|
3441
|
-
function
|
|
3442
|
-
if (
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3635
|
+
function deepCleanObject(obj, options) {
|
|
3636
|
+
if (!obj || typeof obj !== "object") return {};
|
|
3637
|
+
return cleanOwnEntries(obj, {
|
|
3638
|
+
...options,
|
|
3639
|
+
walk: createBoundedWalk({
|
|
3640
|
+
subject: "Frontmatter",
|
|
3641
|
+
limits: {
|
|
3642
|
+
maxValues: MAX_FRONTMATTER_VALUES,
|
|
3643
|
+
maxStringChars: MAX_FRONTMATTER_STRING_CHARS,
|
|
3644
|
+
maxDepth: 64
|
|
3645
|
+
},
|
|
3646
|
+
root: obj
|
|
3647
|
+
})
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
/** Drop null and undefined values, recursively. */
|
|
3651
|
+
function deepRemoveNullishObject(obj) {
|
|
3652
|
+
return deepCleanObject(obj, {});
|
|
3454
3653
|
}
|
|
3654
|
+
/** Drop nullish values and collapse every string onto a single line. */
|
|
3455
3655
|
function deepFlattenStringsObject(obj) {
|
|
3456
|
-
|
|
3457
|
-
const result = {};
|
|
3458
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
3459
|
-
const cleaned = deepFlattenStringsValue(val);
|
|
3460
|
-
if (cleaned !== void 0) result[key] = cleaned;
|
|
3461
|
-
}
|
|
3462
|
-
return result;
|
|
3656
|
+
return deepCleanObject(obj, { transformString: (value) => value.replace(/\n+/g, " ").trim() });
|
|
3463
3657
|
}
|
|
3464
3658
|
function stringifyFrontmatter(body, frontmatter, options) {
|
|
3465
3659
|
const { avoidBlockScalars = false } = options ?? {};
|
|
3466
3660
|
const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
|
|
3467
|
-
|
|
3661
|
+
const file = { content: body };
|
|
3662
|
+
if (avoidBlockScalars) return gray_matter.default.stringify(file, cleanFrontmatter, { engines: { yaml: {
|
|
3468
3663
|
parse: (input) => loadYaml(input) ?? {},
|
|
3469
3664
|
stringify: (data) => (0, js_yaml.dump)(data, { lineWidth: -1 })
|
|
3470
3665
|
} } });
|
|
3471
|
-
return gray_matter.default.stringify(
|
|
3666
|
+
return gray_matter.default.stringify(file, cleanFrontmatter);
|
|
3472
3667
|
}
|
|
3473
3668
|
function parseFrontmatter(content, filePath) {
|
|
3474
3669
|
let frontmatter;
|
|
3475
3670
|
let body;
|
|
3476
3671
|
let hasFrontmatter;
|
|
3477
3672
|
try {
|
|
3673
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3674
|
+
if (bounds && bounds.blockEnd - bounds.blockStart > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
|
|
3478
3675
|
const result = (0, gray_matter.default)(content, {});
|
|
3479
|
-
frontmatter = result.data;
|
|
3676
|
+
frontmatter = deepRemoveNullishObject(result.data);
|
|
3480
3677
|
body = result.content;
|
|
3481
3678
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
3482
3679
|
} catch (error) {
|
|
@@ -3484,7 +3681,7 @@ function parseFrontmatter(content, filePath) {
|
|
|
3484
3681
|
throw error;
|
|
3485
3682
|
}
|
|
3486
3683
|
return {
|
|
3487
|
-
frontmatter
|
|
3684
|
+
frontmatter,
|
|
3488
3685
|
body,
|
|
3489
3686
|
hasFrontmatter
|
|
3490
3687
|
};
|
|
@@ -3533,17 +3730,34 @@ function repairFrontmatterLine(line) {
|
|
|
3533
3730
|
};
|
|
3534
3731
|
}
|
|
3535
3732
|
/**
|
|
3536
|
-
*
|
|
3537
|
-
*
|
|
3538
|
-
*
|
|
3733
|
+
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
3734
|
+
* without parsing it. Shared by the size guard in {@link parseFrontmatter} and
|
|
3735
|
+
* the YAML repair pass below, so both agree on exactly what gray-matter would
|
|
3736
|
+
* treat as the block: gray-matter ends it at the first `\n---`, with no
|
|
3737
|
+
* requirement that the delimiter be alone on its line, so a stricter pattern
|
|
3738
|
+
* here would run past gray-matter's delimiter and act on text that is really
|
|
3739
|
+
* the body.
|
|
3539
3740
|
*/
|
|
3540
|
-
function
|
|
3741
|
+
function findFrontmatterBlockBounds(content) {
|
|
3541
3742
|
const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
|
|
3542
3743
|
if (!opening) return;
|
|
3543
3744
|
const blockStart = opening[0].length;
|
|
3544
3745
|
const closing = /\r?\n---/.exec(content.slice(blockStart));
|
|
3545
3746
|
if (!closing) return;
|
|
3546
|
-
|
|
3747
|
+
return {
|
|
3748
|
+
blockStart,
|
|
3749
|
+
blockEnd: blockStart + closing.index
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Quote the unquoted scalars that make a frontmatter block unparseable, or
|
|
3754
|
+
* return `undefined` when there is nothing to repair. Only the frontmatter
|
|
3755
|
+
* block is rewritten; the body is passed through untouched.
|
|
3756
|
+
*/
|
|
3757
|
+
function repairMalformedFrontmatterYaml(content) {
|
|
3758
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3759
|
+
if (!bounds) return;
|
|
3760
|
+
const { blockStart, blockEnd } = bounds;
|
|
3547
3761
|
const block = content.slice(blockStart, blockEnd);
|
|
3548
3762
|
const repairedLines = block.split("\n").map(repairFrontmatterLine);
|
|
3549
3763
|
const repairedBlock = repairedLines.map(({ line }) => line).join("\n");
|
|
@@ -5222,42 +5436,6 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
|
|
|
5222
5436
|
*/
|
|
5223
5437
|
const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5224
5438
|
//#endregion
|
|
5225
|
-
//#region src/utils/prototype-pollution.ts
|
|
5226
|
-
/**
|
|
5227
|
-
* Keys that, if walked into when constructing or merging objects from
|
|
5228
|
-
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
5229
|
-
* chain) and propagate state to every other object in the runtime. Any code
|
|
5230
|
-
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
5231
|
-
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
5232
|
-
*/
|
|
5233
|
-
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
5234
|
-
"__proto__",
|
|
5235
|
-
"constructor",
|
|
5236
|
-
"prototype"
|
|
5237
|
-
]);
|
|
5238
|
-
function isPrototypePollutionKey(key) {
|
|
5239
|
-
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
5240
|
-
}
|
|
5241
|
-
/**
|
|
5242
|
-
* Returns a shallow copy of a record's own entries with every
|
|
5243
|
-
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
5244
|
-
*
|
|
5245
|
-
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
5246
|
-
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
5247
|
-
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
5248
|
-
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
5249
|
-
* prototype). Walking the entries through this helper severs that path while
|
|
5250
|
-
* preserving every legitimate key.
|
|
5251
|
-
*/
|
|
5252
|
-
function omitPrototypePollutionKeys(record) {
|
|
5253
|
-
const sanitized = {};
|
|
5254
|
-
for (const [key, value] of Object.entries(record)) {
|
|
5255
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
5256
|
-
sanitized[key] = value;
|
|
5257
|
-
}
|
|
5258
|
-
return sanitized;
|
|
5259
|
-
}
|
|
5260
|
-
//#endregion
|
|
5261
5439
|
//#region src/utils/jsonc.ts
|
|
5262
5440
|
/**
|
|
5263
5441
|
* Rebuild the parsed value from its own enumerable entries, dropping
|
|
@@ -6008,6 +6186,17 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6008
6186
|
const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
|
|
6009
6187
|
throw new RulesyncSourceNotFoundError(`No ${(0, node_path.join)(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath)} found.`);
|
|
6010
6188
|
}
|
|
6189
|
+
/**
|
|
6190
|
+
* Return one server exactly as authored, before `getMcpServers()` strips
|
|
6191
|
+
* rulesync- and tool-specific fields. Keep this lookup here so every target
|
|
6192
|
+
* that re-merges one of those fields shares the same own-property and
|
|
6193
|
+
* prototype-pollution guards.
|
|
6194
|
+
*/
|
|
6195
|
+
getRawMcpServer(name) {
|
|
6196
|
+
if (isPrototypePollutionKey(name)) return void 0;
|
|
6197
|
+
const mcpServers = isRecord$1(this.json) ? this.json.mcpServers : void 0;
|
|
6198
|
+
return isRecord$1(mcpServers) && Object.hasOwn(mcpServers, name) ? mcpServers[name] : void 0;
|
|
6199
|
+
}
|
|
6011
6200
|
getMcpServers() {
|
|
6012
6201
|
const mcpServers = this.json.mcpServers ?? {};
|
|
6013
6202
|
const entries = Object.entries(mcpServers);
|
|
@@ -7634,6 +7823,11 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
7634
7823
|
globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7635
7824
|
agentsmd: zod_mini.z.optional(zod_mini.z.looseObject({ subprojectPath: zod_mini.z.optional(zod_mini.z.string()) })),
|
|
7636
7825
|
claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
|
|
7826
|
+
codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7827
|
+
paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7828
|
+
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7829
|
+
description: zod_mini.z.optional(zod_mini.z.string())
|
|
7830
|
+
})),
|
|
7637
7831
|
cursor: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7638
7832
|
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7639
7833
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -7675,7 +7869,8 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
7675
7869
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
7676
7870
|
extends: zod_mini.z.optional(zod_mini.z.string()),
|
|
7677
7871
|
facet: zod_mini.z.optional(zod_mini.z.enum(["policies", "output-contracts"]))
|
|
7678
|
-
}))
|
|
7872
|
+
})),
|
|
7873
|
+
factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({ channel: zod_mini.z.optional(zod_mini.z.enum(["design"])) }))
|
|
7679
7874
|
});
|
|
7680
7875
|
/**
|
|
7681
7876
|
* The `agentsmd.subprojectPath` every consumer should act on, resolved once so
|
|
@@ -8790,15 +8985,15 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8790
8985
|
})),
|
|
8791
8986
|
opencode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8792
8987
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8793
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
8794
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
8795
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
8988
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8989
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8990
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
8796
8991
|
})),
|
|
8797
8992
|
kilo: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8798
8993
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8799
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
8800
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
8801
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
8994
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8995
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
8996
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
8802
8997
|
})),
|
|
8803
8998
|
kiro: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8804
8999
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -8807,9 +9002,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8807
9002
|
})),
|
|
8808
9003
|
deepagents: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8809
9004
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8810
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
8811
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
8812
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
9005
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
9006
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
9007
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
8813
9008
|
})),
|
|
8814
9009
|
copilot: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8815
9010
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -8916,6 +9111,13 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8916
9111
|
takt: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8917
9112
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
8918
9113
|
extends: zod_mini.z.optional(zod_mini.z.string())
|
|
9114
|
+
})),
|
|
9115
|
+
crush: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
9116
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9117
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9118
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
9119
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
9120
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
8919
9121
|
}))
|
|
8920
9122
|
});
|
|
8921
9123
|
/**
|
|
@@ -9141,7 +9343,7 @@ async function getLocalSkillDirNames(sourceTree) {
|
|
|
9141
9343
|
*
|
|
9142
9344
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
9143
9345
|
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
9144
|
-
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9346
|
+
* copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9145
9347
|
* default with a per-target value. A defined section value (including `false`)
|
|
9146
9348
|
* always wins over the root default.
|
|
9147
9349
|
*
|
|
@@ -9160,7 +9362,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
9160
9362
|
*
|
|
9161
9363
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
9162
9364
|
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
9163
|
-
* copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9365
|
+
* copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9164
9366
|
* per-target value. A defined section value (including `false`) always wins
|
|
9165
9367
|
* over the root default.
|
|
9166
9368
|
*
|
|
@@ -9625,8 +9827,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
9625
9827
|
* This only deletes files that are no longer in the rulesync source, not files that will be overwritten.
|
|
9626
9828
|
*/
|
|
9627
9829
|
async removeOrphanAiFiles(existingFiles, generatedFiles) {
|
|
9628
|
-
const generatedPaths = new Set(generatedFiles.map((f) => f.getFilePath()));
|
|
9629
|
-
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(f.getFilePath()));
|
|
9830
|
+
const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
|
|
9831
|
+
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
|
|
9630
9832
|
for (const aiFile of orphanFiles) {
|
|
9631
9833
|
const filePath = aiFile.getFilePath();
|
|
9632
9834
|
const loggedPath = stripControlCharacters(filePath);
|
|
@@ -10753,6 +10955,15 @@ const FACTORYDROID_COMMANDS_DIR_PATH = (0, node_path.join)(FACTORYDROID_DIR, "co
|
|
|
10753
10955
|
const FACTORYDROID_SKILLS_DIR_PATH = (0, node_path.join)(FACTORYDROID_DIR, "skills");
|
|
10754
10956
|
const FACTORYDROID_DROIDS_DIR_PATH = (0, node_path.join)(FACTORYDROID_DIR, "droids");
|
|
10755
10957
|
const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
|
|
10958
|
+
/**
|
|
10959
|
+
* Factory Droid's design-guidelines instruction file: "Always-on design-system,
|
|
10960
|
+
* UX, visual, and interaction guidance", loaded separately from `AGENTS.md`'s
|
|
10961
|
+
* coding guidelines. Project scope only — Factory's docs describe root and
|
|
10962
|
+
* nested `DESIGN.md` files like `AGENTS.md`, but document no personal/global
|
|
10963
|
+
* home-directory equivalent.
|
|
10964
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
10965
|
+
*/
|
|
10966
|
+
const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
|
|
10756
10967
|
const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
|
|
10757
10968
|
const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
|
|
10758
10969
|
const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
@@ -11275,6 +11486,21 @@ function stripStrings(_key, value) {
|
|
|
11275
11486
|
//#endregion
|
|
11276
11487
|
//#region src/features/shared/shared-config-gateway.ts
|
|
11277
11488
|
/**
|
|
11489
|
+
* Upper bound on the number of values a shared config document may expand
|
|
11490
|
+
* to once every YAML alias is written out. Real config files hold a few
|
|
11491
|
+
* hundred values at most; even a large MCP server catalog stays orders of
|
|
11492
|
+
* magnitude below the limit.
|
|
11493
|
+
*/
|
|
11494
|
+
const MAX_SHARED_CONFIG_VALUES = 1e5;
|
|
11495
|
+
/**
|
|
11496
|
+
* Upper bound on the total character count of the string leaves and keys a
|
|
11497
|
+
* shared config document may expand to. The value budget bounds how many
|
|
11498
|
+
* values are visited, but one long string aliased thousands of times fits
|
|
11499
|
+
* that budget while the duplicated output balloons; charging every visited
|
|
11500
|
+
* string's length separately bounds the output regardless of alias count.
|
|
11501
|
+
*/
|
|
11502
|
+
const MAX_SHARED_CONFIG_STRING_CHARS = 4e6;
|
|
11503
|
+
/**
|
|
11278
11504
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
11279
11505
|
*
|
|
11280
11506
|
* Every object is rebuilt, not just the ones that are already plain: a literal
|
|
@@ -11289,15 +11515,61 @@ function stripStrings(_key, value) {
|
|
|
11289
11515
|
*
|
|
11290
11516
|
* Dates are the one object the YAML and TOML parsers produce that is not a
|
|
11291
11517
|
* mapping, so they are passed through rather than flattened into `{}`.
|
|
11518
|
+
*
|
|
11519
|
+
* The rebuild is bounded, because a YAML alias makes one parsed container
|
|
11520
|
+
* reachable from many keys and every alias is copied out independently (the
|
|
11521
|
+
* writers dump with `noRefs: true`, so memoizing here would only move the
|
|
11522
|
+
* blowup into serialization). A small "alias bomb" of nested anchors would
|
|
11523
|
+
* otherwise cost exponential time and memory, and a self-referencing anchor
|
|
11524
|
+
* would recurse until the stack overflowed — both reachable from a config
|
|
11525
|
+
* file committed to a cloned repository. The walk therefore charges every
|
|
11526
|
+
* value against {@link MAX_SHARED_CONFIG_VALUES}, every string and key
|
|
11527
|
+
* against {@link MAX_SHARED_CONFIG_STRING_CHARS}, caps nesting at
|
|
11528
|
+
* {@link MAX_SHARED_CONFIG_DEPTH}, and refuses a reference back to an
|
|
11529
|
+
* ancestor outright, each with a clear error instead of a hang or a crash.
|
|
11292
11530
|
*/
|
|
11293
11531
|
function sanitizeSharedConfigValue(value) {
|
|
11294
|
-
|
|
11532
|
+
return sanitizeSharedConfigValueBounded(value, createBoundedWalk({
|
|
11533
|
+
subject: "Shared config",
|
|
11534
|
+
limits: {
|
|
11535
|
+
maxValues: MAX_SHARED_CONFIG_VALUES,
|
|
11536
|
+
maxStringChars: MAX_SHARED_CONFIG_STRING_CHARS,
|
|
11537
|
+
maxDepth: 64
|
|
11538
|
+
}
|
|
11539
|
+
}));
|
|
11540
|
+
}
|
|
11541
|
+
/**
|
|
11542
|
+
* Refuse a container that is already on the descent path. Unlike the
|
|
11543
|
+
* frontmatter cleaner, which drops such a cycle and keeps the rest of the
|
|
11544
|
+
* document, a shared config file is refused outright: silently dropping part
|
|
11545
|
+
* of a user's settings file would let a later write-back persist the loss.
|
|
11546
|
+
*/
|
|
11547
|
+
function enterSharedConfigContainer(walk, container) {
|
|
11548
|
+
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");
|
|
11549
|
+
walk.enter(container);
|
|
11550
|
+
}
|
|
11551
|
+
function sanitizeSharedConfigValueBounded(value, walk) {
|
|
11552
|
+
if (typeof value === "string") {
|
|
11553
|
+
walk.chargeValue(value.length);
|
|
11554
|
+
return value;
|
|
11555
|
+
}
|
|
11556
|
+
walk.chargeValue();
|
|
11557
|
+
if (Array.isArray(value)) {
|
|
11558
|
+
enterSharedConfigContainer(walk, value);
|
|
11559
|
+
const items = value.map((item) => sanitizeSharedConfigValueBounded(item, walk));
|
|
11560
|
+
walk.leave(value);
|
|
11561
|
+
return items;
|
|
11562
|
+
}
|
|
11295
11563
|
if (value === null || typeof value !== "object" || value instanceof Date) return value;
|
|
11564
|
+
enterSharedConfigContainer(walk, value);
|
|
11296
11565
|
const result = {};
|
|
11297
11566
|
for (const [key, nested] of Object.entries(value)) {
|
|
11567
|
+
walk.chargeChars(key.length);
|
|
11568
|
+
const sanitized = sanitizeSharedConfigValueBounded(nested, walk);
|
|
11298
11569
|
if (isPrototypePollutionKey(key)) continue;
|
|
11299
|
-
result[key] =
|
|
11570
|
+
result[key] = sanitized;
|
|
11300
11571
|
}
|
|
11572
|
+
walk.leave(value);
|
|
11301
11573
|
return result;
|
|
11302
11574
|
}
|
|
11303
11575
|
/**
|
|
@@ -11326,7 +11598,12 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
11326
11598
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11327
11599
|
}
|
|
11328
11600
|
if (parsed === void 0 || parsed === null) return {};
|
|
11329
|
-
|
|
11601
|
+
let sanitized;
|
|
11602
|
+
try {
|
|
11603
|
+
sanitized = sanitizeSharedConfigValue(parsed);
|
|
11604
|
+
} catch (error) {
|
|
11605
|
+
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11606
|
+
}
|
|
11330
11607
|
if (!isPlainObject$1(sanitized)) {
|
|
11331
11608
|
if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
|
|
11332
11609
|
return {};
|
|
@@ -15652,6 +15929,10 @@ function toAllowedToolsArray(value) {
|
|
|
15652
15929
|
* The spec types `compatibility` as a free-form string. An object from a legacy
|
|
15653
15930
|
* rulesync input is flattened to `key: value` pairs instead of being emitted as
|
|
15654
15931
|
* a YAML mapping, which conformant clients reject.
|
|
15932
|
+
*
|
|
15933
|
+
* Exported for `CrushSkill`, which requires the same bare-string shape (Crush's
|
|
15934
|
+
* Go struct types `Compatibility` as a plain `string`) and reuses this
|
|
15935
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15655
15936
|
*/
|
|
15656
15937
|
function toCompatibilityString(value) {
|
|
15657
15938
|
if (typeof value === "string") return value;
|
|
@@ -15660,6 +15941,10 @@ function toCompatibilityString(value) {
|
|
|
15660
15941
|
/**
|
|
15661
15942
|
* The spec types `metadata` as "a map from string keys to string values", so
|
|
15662
15943
|
* non-string values (e.g. a YAML number `version: 1`) are stringified.
|
|
15944
|
+
*
|
|
15945
|
+
* Exported for `CrushSkill`, which requires the same `map[string]string`
|
|
15946
|
+
* shape (Crush's Go struct types `Metadata` that way) and reuses this
|
|
15947
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15663
15948
|
*/
|
|
15664
15949
|
function toStringMetadata(metadata) {
|
|
15665
15950
|
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
|
|
@@ -18670,7 +18955,13 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18670
18955
|
if (!matchByBasename || flatOnly && (0, node_path.dirname)(key) !== ".") return [key];
|
|
18671
18956
|
return [key, (0, node_path.basename)(key)];
|
|
18672
18957
|
};
|
|
18673
|
-
const
|
|
18958
|
+
const claimedKeys = new ClaimedIdentities();
|
|
18959
|
+
const primarySource = paths.relativeDirPath;
|
|
18960
|
+
const secondarySource = "a secondary source";
|
|
18961
|
+
for (const command of toolCommands) for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18962
|
+
identity: candidate,
|
|
18963
|
+
source: primarySource
|
|
18964
|
+
});
|
|
18674
18965
|
const additionalCommands = await factory.class.loadAdditionalImportFiles({
|
|
18675
18966
|
outputRoot: this.outputRoot,
|
|
18676
18967
|
global: this.global,
|
|
@@ -18678,11 +18969,26 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18678
18969
|
});
|
|
18679
18970
|
for (const command of additionalCommands) {
|
|
18680
18971
|
const key = command.getRelativeFilePath();
|
|
18681
|
-
|
|
18682
|
-
|
|
18972
|
+
const collision = [...new Set(keysOf(command, true))].map((candidate) => {
|
|
18973
|
+
const claimed = claimedKeys.claim({
|
|
18974
|
+
identity: candidate,
|
|
18975
|
+
source: secondarySource
|
|
18976
|
+
});
|
|
18977
|
+
return claimed === null ? void 0 : {
|
|
18978
|
+
candidate,
|
|
18979
|
+
claimed
|
|
18980
|
+
};
|
|
18981
|
+
}).find((hit) => hit !== void 0);
|
|
18982
|
+
if (collision) {
|
|
18983
|
+
const { candidate, claimed } = collision;
|
|
18984
|
+
if (claimed.spelling === candidate) this.logger.warn(`Duplicate ${this.toolTarget} command "${stripControlCharacters(key)}" from ${secondarySource}; keeping the one already loaded.`);
|
|
18985
|
+
else this.logger.warn(`Case-insensitive ${this.toolTarget} command collision: "${stripControlCharacters(claimed.spelling)}" and "${stripControlCharacters(candidate)}" resolve to the same command file. Keeping "${stripControlCharacters(claimed.spelling)}" from ${claimed.source === secondarySource ? "earlier in the same source" : `the higher-precedence ${claimed.source}`} and ignoring "${stripControlCharacters(key)}" from ${secondarySource}, which is not imported.`);
|
|
18683
18986
|
continue;
|
|
18684
18987
|
}
|
|
18685
|
-
for (const candidate of keysOf(command))
|
|
18988
|
+
for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18989
|
+
identity: candidate,
|
|
18990
|
+
source: secondarySource
|
|
18991
|
+
});
|
|
18686
18992
|
toolCommands.push(command);
|
|
18687
18993
|
}
|
|
18688
18994
|
}
|
|
@@ -18972,6 +19278,21 @@ var AmpHooks = class AmpHooks extends ToolHooks {
|
|
|
18972
19278
|
}
|
|
18973
19279
|
};
|
|
18974
19280
|
//#endregion
|
|
19281
|
+
//#region src/utils/own-lookup.ts
|
|
19282
|
+
/**
|
|
19283
|
+
* Read a key from a plain string map without walking its prototype chain.
|
|
19284
|
+
*
|
|
19285
|
+
* A bracket read on an object literal resolves inherited members too, so a
|
|
19286
|
+
* user-supplied key such as `toString` or `constructor` "succeeds" with an
|
|
19287
|
+
* `Object.prototype` function instead of falling through to the caller's
|
|
19288
|
+
* `?? fallback`. Hook adapters translate native event names this way from
|
|
19289
|
+
* `Object.entries()` over a config file, so route the read through here to keep
|
|
19290
|
+
* the fallback honest: only a key the map itself defines yields a value.
|
|
19291
|
+
*/
|
|
19292
|
+
function lookupOwn({ record, key }) {
|
|
19293
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
19294
|
+
}
|
|
19295
|
+
//#endregion
|
|
18975
19296
|
//#region src/utils/object.ts
|
|
18976
19297
|
/**
|
|
18977
19298
|
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
@@ -19356,7 +19677,10 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
19356
19677
|
const warn = warnOnce(logger);
|
|
19357
19678
|
const result = {};
|
|
19358
19679
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
19359
|
-
const toolEventName =
|
|
19680
|
+
const toolEventName = lookupOwn({
|
|
19681
|
+
record: converterConfig.canonicalToToolEventNames,
|
|
19682
|
+
key: eventName
|
|
19683
|
+
}) ?? eventName;
|
|
19360
19684
|
const byMatcher = groupDefinitionsByMatcher({
|
|
19361
19685
|
definitions,
|
|
19362
19686
|
converterConfig
|
|
@@ -19784,7 +20108,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
19784
20108
|
const warn = warnOnce(logger);
|
|
19785
20109
|
const canonical = {};
|
|
19786
20110
|
for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
|
|
19787
|
-
const eventName =
|
|
20111
|
+
const eventName = lookupOwn({
|
|
20112
|
+
record: converterConfig.toolToCanonicalEventNames,
|
|
20113
|
+
key: toolEventName
|
|
20114
|
+
}) ?? toolEventName;
|
|
19788
20115
|
if (!Array.isArray(matcherEntries)) continue;
|
|
19789
20116
|
const defs = [];
|
|
19790
20117
|
for (const rawEntry of matcherEntries) {
|
|
@@ -19840,7 +20167,10 @@ function flattenAntigravityHooks(parsed) {
|
|
|
19840
20167
|
const flat = {};
|
|
19841
20168
|
const addEvent = (event, entries) => {
|
|
19842
20169
|
if (isPrototypePollutionKey(event) || !Array.isArray(entries)) return;
|
|
19843
|
-
const existing =
|
|
20170
|
+
const existing = lookupOwn({
|
|
20171
|
+
record: flat,
|
|
20172
|
+
key: event
|
|
20173
|
+
});
|
|
19844
20174
|
flat[event] = existing ? [...existing, ...entries] : [...entries];
|
|
19845
20175
|
};
|
|
19846
20176
|
for (const [key, value] of Object.entries(parsed)) if (Array.isArray(value)) addEvent(key, value);
|
|
@@ -20227,6 +20557,16 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20227
20557
|
const paths = AugmentcodeHooks.getSettablePaths({ global });
|
|
20228
20558
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
20229
20559
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
20560
|
+
let existingHooks = {};
|
|
20561
|
+
try {
|
|
20562
|
+
const parsed = JSON.parse(existingContent);
|
|
20563
|
+
const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
|
|
20564
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
|
|
20565
|
+
} catch {
|
|
20566
|
+
existingHooks = {};
|
|
20567
|
+
}
|
|
20568
|
+
const nativeEventKeys = new Set(Object.values(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES));
|
|
20569
|
+
const preservedHooks = Object.fromEntries(Object.entries(existingHooks).filter(([key]) => !nativeEventKeys.has(key)));
|
|
20230
20570
|
const config = rulesyncHooks.getJson();
|
|
20231
20571
|
const augmentHooks = canonicalToToolHooks({
|
|
20232
20572
|
config,
|
|
@@ -20238,7 +20578,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20238
20578
|
fileKey: sharedConfigFileKey(paths),
|
|
20239
20579
|
feature: "hooks",
|
|
20240
20580
|
existingContent,
|
|
20241
|
-
patch: { hooks:
|
|
20581
|
+
patch: { hooks: {
|
|
20582
|
+
...preservedHooks,
|
|
20583
|
+
...augmentHooks
|
|
20584
|
+
} },
|
|
20242
20585
|
filePath
|
|
20243
20586
|
});
|
|
20244
20587
|
return new AugmentcodeHooks({
|
|
@@ -20996,7 +21339,10 @@ function canonicalToCopilotHooks(config) {
|
|
|
20996
21339
|
};
|
|
20997
21340
|
const copilot = {};
|
|
20998
21341
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
20999
|
-
const copilotEventName =
|
|
21342
|
+
const copilotEventName = lookupOwn({
|
|
21343
|
+
record: CANONICAL_TO_COPILOT_EVENT_NAMES,
|
|
21344
|
+
key: eventName
|
|
21345
|
+
}) ?? eventName;
|
|
21000
21346
|
const entries = [];
|
|
21001
21347
|
for (const def of definitions) {
|
|
21002
21348
|
const hookType = def.type ?? "command";
|
|
@@ -21073,7 +21419,10 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
21073
21419
|
if (copilotHooks === null || copilotHooks === void 0 || typeof copilotHooks !== "object") return {};
|
|
21074
21420
|
const canonical = {};
|
|
21075
21421
|
for (const [copilotEventName, hookEntries] of Object.entries(copilotHooks)) {
|
|
21076
|
-
const eventName =
|
|
21422
|
+
const eventName = lookupOwn({
|
|
21423
|
+
record: COPILOT_TO_CANONICAL_EVENT_NAMES,
|
|
21424
|
+
key: copilotEventName
|
|
21425
|
+
}) ?? copilotEventName;
|
|
21077
21426
|
if (!Array.isArray(hookEntries)) continue;
|
|
21078
21427
|
const defs = [];
|
|
21079
21428
|
for (const rawEntry of hookEntries) {
|
|
@@ -21343,7 +21692,10 @@ function canonicalToCopilotCliHooks(config, logger) {
|
|
|
21343
21692
|
};
|
|
21344
21693
|
const out = {};
|
|
21345
21694
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
21346
|
-
const copilotEventName =
|
|
21695
|
+
const copilotEventName = lookupOwn({
|
|
21696
|
+
record: CANONICAL_TO_COPILOTCLI_EVENT_NAMES,
|
|
21697
|
+
key: eventName
|
|
21698
|
+
}) ?? eventName;
|
|
21347
21699
|
const entries = buildCopilotCliEntriesForEvent({
|
|
21348
21700
|
eventName,
|
|
21349
21701
|
definitions,
|
|
@@ -21400,7 +21752,10 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
|
|
|
21400
21752
|
if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
|
|
21401
21753
|
const canonical = {};
|
|
21402
21754
|
for (const [copilotEventName, hookEntries] of Object.entries(rawHooks)) {
|
|
21403
|
-
const eventName =
|
|
21755
|
+
const eventName = lookupOwn({
|
|
21756
|
+
record: COPILOTCLI_TO_CANONICAL_EVENT_NAMES,
|
|
21757
|
+
key: copilotEventName
|
|
21758
|
+
}) ?? copilotEventName;
|
|
21404
21759
|
if (!Array.isArray(hookEntries)) continue;
|
|
21405
21760
|
const defs = [];
|
|
21406
21761
|
for (const rawEntry of hookEntries) {
|
|
@@ -21552,7 +21907,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21552
21907
|
const mappedHooks = {};
|
|
21553
21908
|
const cursorSupportedTypes = /* @__PURE__ */ new Set(["command", "prompt"]);
|
|
21554
21909
|
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
21555
|
-
const cursorEventName =
|
|
21910
|
+
const cursorEventName = lookupOwn({
|
|
21911
|
+
record: CANONICAL_TO_CURSOR_EVENT_NAMES,
|
|
21912
|
+
key: eventName
|
|
21913
|
+
}) ?? eventName;
|
|
21556
21914
|
const mappedDefs = defs.filter((def) => cursorSupportedTypes.has(def.type ?? "command")).map((def) => ({
|
|
21557
21915
|
...def.type !== void 0 && def.type !== null && { type: def.type },
|
|
21558
21916
|
...def.command !== void 0 && def.command !== null && { command: def.command },
|
|
@@ -21585,7 +21943,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21585
21943
|
const cursorHooks = parsed.hooks ?? {};
|
|
21586
21944
|
const canonicalHooks = {};
|
|
21587
21945
|
for (const [cursorEventName, defs] of Object.entries(cursorHooks)) {
|
|
21588
|
-
const eventName =
|
|
21946
|
+
const eventName = lookupOwn({
|
|
21947
|
+
record: CURSOR_TO_CANONICAL_EVENT_NAMES,
|
|
21948
|
+
key: cursorEventName
|
|
21949
|
+
}) ?? cursorEventName;
|
|
21589
21950
|
canonicalHooks[eventName] = defs;
|
|
21590
21951
|
}
|
|
21591
21952
|
const version = parsed.version ?? 1;
|
|
@@ -21655,7 +22016,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21655
22016
|
const hooks = {};
|
|
21656
22017
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
21657
22018
|
if (!supported.has(canonicalEvent)) continue;
|
|
21658
|
-
const deepagentsEvent =
|
|
22019
|
+
const deepagentsEvent = lookupOwn({
|
|
22020
|
+
record: CANONICAL_TO_DEEPAGENTS_EVENT_NAMES,
|
|
22021
|
+
key: canonicalEvent
|
|
22022
|
+
});
|
|
21659
22023
|
if (!deepagentsEvent) continue;
|
|
21660
22024
|
for (const def of definitions) {
|
|
21661
22025
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -21684,7 +22048,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21684
22048
|
function deepagentsToCanonicalHooks(hooks) {
|
|
21685
22049
|
const canonical = {};
|
|
21686
22050
|
for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
|
|
21687
|
-
const canonicalEvent =
|
|
22051
|
+
const canonicalEvent = lookupOwn({
|
|
22052
|
+
record: DEEPAGENTS_TO_CANONICAL_EVENT_NAMES,
|
|
22053
|
+
key: deepagentsEvent
|
|
22054
|
+
});
|
|
21688
22055
|
if (!canonicalEvent || !Array.isArray(groups)) continue;
|
|
21689
22056
|
for (const group of groups) {
|
|
21690
22057
|
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
@@ -21717,7 +22084,10 @@ function deepagentsLegacyToCanonicalHooks(entries) {
|
|
|
21717
22084
|
const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
|
|
21718
22085
|
const events = Array.isArray(entry.events) ? entry.events : [];
|
|
21719
22086
|
for (const legacyEvent of events) {
|
|
21720
|
-
const canonicalEvent = typeof legacyEvent === "string" ?
|
|
22087
|
+
const canonicalEvent = typeof legacyEvent === "string" ? lookupOwn({
|
|
22088
|
+
record: DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES,
|
|
22089
|
+
key: legacyEvent
|
|
22090
|
+
}) : void 0;
|
|
21721
22091
|
if (!canonicalEvent) continue;
|
|
21722
22092
|
(canonical[canonicalEvent] ??= []).push({
|
|
21723
22093
|
type: "command",
|
|
@@ -22438,7 +22808,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22438
22808
|
const result = {};
|
|
22439
22809
|
for (const [canonicalEvent, definitions] of Object.entries(config.hooks)) {
|
|
22440
22810
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22441
|
-
const nativeEvent =
|
|
22811
|
+
const nativeEvent = lookupOwn({
|
|
22812
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22813
|
+
key: canonicalEvent
|
|
22814
|
+
});
|
|
22442
22815
|
if (nativeEvent) setHermesHookEntries({
|
|
22443
22816
|
result,
|
|
22444
22817
|
event: nativeEvent,
|
|
@@ -22449,7 +22822,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22449
22822
|
}
|
|
22450
22823
|
for (const [canonicalEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
22451
22824
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22452
|
-
const nativeEvent =
|
|
22825
|
+
const nativeEvent = lookupOwn({
|
|
22826
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22827
|
+
key: canonicalEvent
|
|
22828
|
+
});
|
|
22453
22829
|
if (nativeEvent) setHermesHookEntries({
|
|
22454
22830
|
result,
|
|
22455
22831
|
event: nativeEvent,
|
|
@@ -22515,7 +22891,10 @@ function hermesHooksToCanonical(hooks) {
|
|
|
22515
22891
|
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
22516
22892
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
22517
22893
|
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
22518
|
-
const rulesyncEvent =
|
|
22894
|
+
const rulesyncEvent = lookupOwn({
|
|
22895
|
+
record: HERMESAGENT_TO_CANONICAL_EVENT_NAMES,
|
|
22896
|
+
key: nativeEvent
|
|
22897
|
+
}) ?? nativeEvent;
|
|
22519
22898
|
const defs = entries.map((raw) => hermesEntryToDefinition({
|
|
22520
22899
|
nativeEvent,
|
|
22521
22900
|
raw
|
|
@@ -23152,7 +23531,10 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
|
|
|
23152
23531
|
const result = [];
|
|
23153
23532
|
const nativeEvents = new Set(KIMI_CODE_NATIVE_HOOK_EVENTS);
|
|
23154
23533
|
for (const [event, definitions] of Object.entries(buildEffectiveHooks(config, toolOverrideHooks))) {
|
|
23155
|
-
const nativeEvent =
|
|
23534
|
+
const nativeEvent = lookupOwn({
|
|
23535
|
+
record: CANONICAL_TO_KIMI_CODE_EVENT_NAMES,
|
|
23536
|
+
key: event
|
|
23537
|
+
}) ?? event;
|
|
23156
23538
|
if (!nativeEvents.has(nativeEvent)) {
|
|
23157
23539
|
logger?.warn(`Kimi Code hooks: skipping unsupported event "${event}".`);
|
|
23158
23540
|
continue;
|
|
@@ -23186,14 +23568,22 @@ function kimiCodeHooksToCanonical(hooks) {
|
|
|
23186
23568
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
23187
23569
|
const entry = raw;
|
|
23188
23570
|
if (typeof entry.event !== "string" || typeof entry.command !== "string") continue;
|
|
23189
|
-
const event =
|
|
23571
|
+
const event = lookupOwn({
|
|
23572
|
+
record: KIMI_CODE_TO_CANONICAL_EVENT_NAMES,
|
|
23573
|
+
key: entry.event
|
|
23574
|
+
}) ?? entry.event;
|
|
23190
23575
|
const definition = {
|
|
23191
23576
|
type: "command",
|
|
23192
23577
|
command: stripTrustedDirectoryWrapper(entry.command),
|
|
23193
23578
|
...typeof entry.matcher === "string" && { matcher: entry.matcher },
|
|
23194
23579
|
...typeof entry.timeout === "number" && { timeout: entry.timeout }
|
|
23195
23580
|
};
|
|
23196
|
-
|
|
23581
|
+
const list = lookupOwn({
|
|
23582
|
+
record: result,
|
|
23583
|
+
key: event
|
|
23584
|
+
}) ?? [];
|
|
23585
|
+
list.push(definition);
|
|
23586
|
+
result[event] = list;
|
|
23197
23587
|
}
|
|
23198
23588
|
return result;
|
|
23199
23589
|
}
|
|
@@ -23396,7 +23786,13 @@ function canonicalToKiroIdeHooks(config) {
|
|
|
23396
23786
|
};
|
|
23397
23787
|
const entries = [];
|
|
23398
23788
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23399
|
-
const trigger =
|
|
23789
|
+
const trigger = lookupOwn({
|
|
23790
|
+
record: CANONICAL_TO_KIRO_IDE_EVENT_NAMES,
|
|
23791
|
+
key: eventName
|
|
23792
|
+
}) ?? lookupOwn({
|
|
23793
|
+
record: KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES,
|
|
23794
|
+
key: eventName
|
|
23795
|
+
}) ?? eventName;
|
|
23400
23796
|
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
23401
23797
|
}
|
|
23402
23798
|
return entries;
|
|
@@ -23405,7 +23801,10 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23405
23801
|
const canonical = {};
|
|
23406
23802
|
for (const entry of entries) {
|
|
23407
23803
|
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
23408
|
-
const eventName =
|
|
23804
|
+
const eventName = lookupOwn({
|
|
23805
|
+
record: KIRO_IDE_TO_CANONICAL_EVENT_NAMES,
|
|
23806
|
+
key: entry.trigger
|
|
23807
|
+
}) ?? entry.trigger;
|
|
23409
23808
|
if (isPrototypePollutionKey(eventName)) continue;
|
|
23410
23809
|
const def = {};
|
|
23411
23810
|
if (entry.action.type === "command") {
|
|
@@ -23422,7 +23821,12 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23422
23821
|
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
23423
23822
|
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
23424
23823
|
if (entry.enabled === false) def.enabled = false;
|
|
23425
|
-
|
|
23824
|
+
const list = lookupOwn({
|
|
23825
|
+
record: canonical,
|
|
23826
|
+
key: eventName
|
|
23827
|
+
}) ?? [];
|
|
23828
|
+
list.push(def);
|
|
23829
|
+
canonical[eventName] = list;
|
|
23426
23830
|
}
|
|
23427
23831
|
return canonical;
|
|
23428
23832
|
}
|
|
@@ -23603,7 +24007,10 @@ function canonicalToKiroHooks({ config, logger }) {
|
|
|
23603
24007
|
};
|
|
23604
24008
|
const kiro = {};
|
|
23605
24009
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23606
|
-
const kiroEventName =
|
|
24010
|
+
const kiroEventName = lookupOwn({
|
|
24011
|
+
record: CANONICAL_TO_KIRO_EVENT_NAMES,
|
|
24012
|
+
key: eventName
|
|
24013
|
+
}) ?? eventName;
|
|
23607
24014
|
const entries = buildKiroEntriesForEvent(definitions);
|
|
23608
24015
|
if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
|
|
23609
24016
|
else kiro[kiroEventName] = entries;
|
|
@@ -23634,7 +24041,10 @@ function kiroHooksToCanonical(kiroHooks) {
|
|
|
23634
24041
|
if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
|
|
23635
24042
|
const canonical = {};
|
|
23636
24043
|
for (const [kiroEventName, entries] of Object.entries(kiroHooks)) {
|
|
23637
|
-
const eventName =
|
|
24044
|
+
const eventName = lookupOwn({
|
|
24045
|
+
record: KIRO_TO_CANONICAL_EVENT_NAMES,
|
|
24046
|
+
key: kiroEventName
|
|
24047
|
+
}) ?? kiroEventName;
|
|
23638
24048
|
if (!Array.isArray(entries)) continue;
|
|
23639
24049
|
const defs = [];
|
|
23640
24050
|
for (const rawEntry of entries) {
|
|
@@ -24193,7 +24603,10 @@ function canonicalToQwencodeHooks(config, logger) {
|
|
|
24193
24603
|
]);
|
|
24194
24604
|
const qwencode = {};
|
|
24195
24605
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
24196
|
-
const qwencodeEventName =
|
|
24606
|
+
const qwencodeEventName = lookupOwn({
|
|
24607
|
+
record: CANONICAL_TO_QWENCODE_EVENT_NAMES,
|
|
24608
|
+
key: eventName
|
|
24609
|
+
}) ?? eventName;
|
|
24197
24610
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
24198
24611
|
for (const def of definitions) {
|
|
24199
24612
|
if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
|
|
@@ -24298,7 +24711,10 @@ function qwencodeHooksToCanonical(qwencodeHooks) {
|
|
|
24298
24711
|
if (qwencodeHooks === null || qwencodeHooks === void 0 || typeof qwencodeHooks !== "object") return {};
|
|
24299
24712
|
const canonical = {};
|
|
24300
24713
|
for (const [qwencodeEventName, matcherEntries] of Object.entries(qwencodeHooks)) {
|
|
24301
|
-
const eventName =
|
|
24714
|
+
const eventName = lookupOwn({
|
|
24715
|
+
record: QWENCODE_TO_CANONICAL_EVENT_NAMES,
|
|
24716
|
+
key: qwencodeEventName
|
|
24717
|
+
}) ?? qwencodeEventName;
|
|
24302
24718
|
if (!Array.isArray(matcherEntries)) continue;
|
|
24303
24719
|
const defs = [];
|
|
24304
24720
|
for (const rawEntry of matcherEntries) {
|
|
@@ -24413,7 +24829,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24413
24829
|
const result = {};
|
|
24414
24830
|
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
24415
24831
|
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
24416
|
-
const reasonixEvent =
|
|
24832
|
+
const reasonixEvent = lookupOwn({
|
|
24833
|
+
record: CANONICAL_TO_REASONIX_EVENT_NAMES,
|
|
24834
|
+
key: event
|
|
24835
|
+
}) ?? event;
|
|
24417
24836
|
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
24418
24837
|
const entries = [];
|
|
24419
24838
|
for (const def of defs) {
|
|
@@ -24426,7 +24845,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24426
24845
|
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
24427
24846
|
entries.push(entry);
|
|
24428
24847
|
}
|
|
24429
|
-
if (entries.length > 0) result[reasonixEvent] = [...
|
|
24848
|
+
if (entries.length > 0) result[reasonixEvent] = [...lookupOwn({
|
|
24849
|
+
record: result,
|
|
24850
|
+
key: reasonixEvent
|
|
24851
|
+
}) ?? [], ...entries];
|
|
24430
24852
|
}
|
|
24431
24853
|
return result;
|
|
24432
24854
|
}
|
|
@@ -24439,7 +24861,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24439
24861
|
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
24440
24862
|
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
24441
24863
|
if (!Array.isArray(rawEntries)) continue;
|
|
24442
|
-
const canonicalEvent =
|
|
24864
|
+
const canonicalEvent = lookupOwn({
|
|
24865
|
+
record: REASONIX_TO_CANONICAL_EVENT_NAMES,
|
|
24866
|
+
key: reasonixEvent
|
|
24867
|
+
}) ?? reasonixEvent;
|
|
24443
24868
|
const defs = [];
|
|
24444
24869
|
for (const rawEntry of rawEntries) {
|
|
24445
24870
|
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
@@ -24454,7 +24879,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24454
24879
|
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
24455
24880
|
defs.push(def);
|
|
24456
24881
|
}
|
|
24457
|
-
if (defs.length > 0) canonical[canonicalEvent] = [...
|
|
24882
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...lookupOwn({
|
|
24883
|
+
record: canonical,
|
|
24884
|
+
key: canonicalEvent
|
|
24885
|
+
}) ?? [], ...defs];
|
|
24458
24886
|
}
|
|
24459
24887
|
return canonical;
|
|
24460
24888
|
}
|
|
@@ -24582,7 +25010,10 @@ function canonicalToVibeHooks(config, toolOverride) {
|
|
|
24582
25010
|
const hooks = [];
|
|
24583
25011
|
for (const [event, defs] of Object.entries(effective)) {
|
|
24584
25012
|
if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
|
|
24585
|
-
const vibeEvent =
|
|
25013
|
+
const vibeEvent = lookupOwn({
|
|
25014
|
+
record: CANONICAL_TO_VIBE_EVENT_NAMES,
|
|
25015
|
+
key: event
|
|
25016
|
+
}) ?? event;
|
|
24586
25017
|
let index = 0;
|
|
24587
25018
|
for (const def of defs) {
|
|
24588
25019
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -24612,7 +25043,10 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
24612
25043
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
24613
25044
|
if (vibeEvent === void 0) return null;
|
|
24614
25045
|
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
24615
|
-
const canonicalEvent =
|
|
25046
|
+
const canonicalEvent = lookupOwn({
|
|
25047
|
+
record: VIBE_TO_CANONICAL_EVENT_NAMES,
|
|
25048
|
+
key: vibeEvent
|
|
25049
|
+
}) ?? vibeEvent;
|
|
24616
25050
|
const def = { type: "command" };
|
|
24617
25051
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
24618
25052
|
if (typeof entry.match === "string" && entry.match !== "" && entry.match !== "*") def.matcher = entry.match;
|
|
@@ -24637,7 +25071,10 @@ function vibeHooksToCanonical(parsed) {
|
|
|
24637
25071
|
for (const raw of rawHooks) {
|
|
24638
25072
|
const result = vibeEntryToCanonicalDef(raw);
|
|
24639
25073
|
if (result === null) continue;
|
|
24640
|
-
const list =
|
|
25074
|
+
const list = lookupOwn({
|
|
25075
|
+
record: canonical,
|
|
25076
|
+
key: result.canonicalEvent
|
|
25077
|
+
}) ?? [];
|
|
24641
25078
|
list.push(result.def);
|
|
24642
25079
|
canonical[result.canonicalEvent] = list;
|
|
24643
25080
|
}
|
|
@@ -25716,6 +26153,68 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
|
|
|
25716
26153
|
}
|
|
25717
26154
|
};
|
|
25718
26155
|
//#endregion
|
|
26156
|
+
//#region src/constants/crush-paths.ts
|
|
26157
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
26158
|
+
const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
|
|
26159
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
26160
|
+
const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
|
|
26161
|
+
const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
|
|
26162
|
+
//#endregion
|
|
26163
|
+
//#region src/features/ignore/crush-ignore.ts
|
|
26164
|
+
/**
|
|
26165
|
+
* Ignore generator for Crush.
|
|
26166
|
+
*
|
|
26167
|
+
* Crush excludes files from tool access via a `.crushignore` file, read
|
|
26168
|
+
* hierarchically (root and any subdirectory, the same way it walks
|
|
26169
|
+
* `.gitignore`) using gitignore syntax. Crush documents no global/user-scope
|
|
26170
|
+
* ignore file, so this is project-only.
|
|
26171
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/fsext/fileutil.go
|
|
26172
|
+
*/
|
|
26173
|
+
var CrushIgnore = class CrushIgnore extends ToolIgnore {
|
|
26174
|
+
static getSettablePaths() {
|
|
26175
|
+
return {
|
|
26176
|
+
relativeDirPath: ".",
|
|
26177
|
+
relativeFilePath: CRUSH_IGNORE_FILE_NAME
|
|
26178
|
+
};
|
|
26179
|
+
}
|
|
26180
|
+
toRulesyncIgnore() {
|
|
26181
|
+
return new RulesyncIgnore({
|
|
26182
|
+
outputRoot: ".",
|
|
26183
|
+
relativeDirPath: ".",
|
|
26184
|
+
relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
|
|
26185
|
+
fileContent: this.fileContent
|
|
26186
|
+
});
|
|
26187
|
+
}
|
|
26188
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
26189
|
+
const body = rulesyncIgnore.getFileContent();
|
|
26190
|
+
return new CrushIgnore({
|
|
26191
|
+
outputRoot,
|
|
26192
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26193
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26194
|
+
fileContent: body
|
|
26195
|
+
});
|
|
26196
|
+
}
|
|
26197
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
26198
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
26199
|
+
return new CrushIgnore({
|
|
26200
|
+
outputRoot,
|
|
26201
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26202
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26203
|
+
fileContent,
|
|
26204
|
+
validate
|
|
26205
|
+
});
|
|
26206
|
+
}
|
|
26207
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
26208
|
+
return new CrushIgnore({
|
|
26209
|
+
outputRoot,
|
|
26210
|
+
relativeDirPath,
|
|
26211
|
+
relativeFilePath,
|
|
26212
|
+
fileContent: "",
|
|
26213
|
+
validate: false
|
|
26214
|
+
});
|
|
26215
|
+
}
|
|
26216
|
+
};
|
|
26217
|
+
//#endregion
|
|
25719
26218
|
//#region src/features/ignore/cursor-ignore.ts
|
|
25720
26219
|
/**
|
|
25721
26220
|
* Cursor ignore adapter.
|
|
@@ -26712,6 +27211,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
26712
27211
|
["claudecode", { class: ClaudecodeIgnore }],
|
|
26713
27212
|
["claudecode-legacy", { class: ClaudecodeIgnore }],
|
|
26714
27213
|
["cline", { class: ClineIgnore }],
|
|
27214
|
+
["crush", { class: CrushIgnore }],
|
|
26715
27215
|
["cursor", { class: CursorIgnore }],
|
|
26716
27216
|
["hermesagent", { class: HermesagentIgnore }],
|
|
26717
27217
|
["junie", { class: JunieIgnore }],
|
|
@@ -28016,9 +28516,8 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
28016
28516
|
throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
|
|
28017
28517
|
}
|
|
28018
28518
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
28019
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
28020
28519
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
28021
|
-
const rawServer =
|
|
28520
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
28022
28521
|
return [serverName, {
|
|
28023
28522
|
...serverConfig,
|
|
28024
28523
|
...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
@@ -30981,9 +31480,8 @@ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
|
|
|
30981
31480
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
30982
31481
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
30983
31482
|
const existing = parseMusecodeSettings(existingContent, filePath);
|
|
30984
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
30985
31483
|
const converted = convertToMusecodeFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
30986
|
-
const rawServer =
|
|
31484
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
30987
31485
|
const mode = asMusecodeMode(isRecord$1(rawServer) ? rawServer.musecodeMode : void 0);
|
|
30988
31486
|
return [serverName, {
|
|
30989
31487
|
...serverConfig,
|
|
@@ -31932,6 +32430,66 @@ async function readRovodevConfigYaml({ outputRoot }) {
|
|
|
31932
32430
|
filePath: (0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
|
|
31933
32431
|
});
|
|
31934
32432
|
}
|
|
32433
|
+
/**
|
|
32434
|
+
* Decide the absolute path a configured `mcpConfigPath` would name in the
|
|
32435
|
+
* given scope, without yet checking whether that path stays inside it. Split
|
|
32436
|
+
* out of resolveRovodevMcpImportPath so each function's branching stays
|
|
32437
|
+
* within the project's complexity budget.
|
|
32438
|
+
*/
|
|
32439
|
+
function resolveMcpConfigCandidatePath({ outputRoot, global, normalizedPath, configuredPath }) {
|
|
32440
|
+
if (global && normalizedPath.startsWith("~/")) return { path: (0, node_path.resolve)(outputRoot, normalizedPath.slice(2)) };
|
|
32441
|
+
if (!global && normalizedPath.startsWith("~/")) return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in project scope. A home-anchored path cannot be imported as part of a project, so importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32442
|
+
if ((0, node_path.isAbsolute)(normalizedPath)) return { path: (0, node_path.resolve)(normalizedPath) };
|
|
32443
|
+
if (!global) return { path: (0, node_path.resolve)(outputRoot, normalizedPath) };
|
|
32444
|
+
return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in global scope. Only home-anchored or absolute paths can be imported safely, so importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32445
|
+
}
|
|
32446
|
+
/**
|
|
32447
|
+
* Resolve the active Rovo Dev MCP config without following a pointer outside
|
|
32448
|
+
* the import scope. The implementation is deliberately separate from
|
|
32449
|
+
* fromFile: it keeps path-policy decisions testable without changing the
|
|
32450
|
+
* public ToolMcp contract.
|
|
32451
|
+
*/
|
|
32452
|
+
async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger }) {
|
|
32453
|
+
const fallback = {
|
|
32454
|
+
filePath: (0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_MCP_FILE_NAME),
|
|
32455
|
+
relativeDirPath: ROVODEV_DIR,
|
|
32456
|
+
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32457
|
+
};
|
|
32458
|
+
const configuredPath = (config && isRecord$1(config.mcp) ? config.mcp : {}).mcpConfigPath;
|
|
32459
|
+
if (configuredPath === void 0) {
|
|
32460
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is unset in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)}, which may not be the file Rovo Dev reads.`);
|
|
32461
|
+
return fallback;
|
|
32462
|
+
}
|
|
32463
|
+
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
32464
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} must be a non-empty string. Importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32465
|
+
return fallback;
|
|
32466
|
+
}
|
|
32467
|
+
const normalizedPath = normalizeMcpConfigPathValue(configuredPath.trim());
|
|
32468
|
+
const candidateResult = resolveMcpConfigCandidatePath({
|
|
32469
|
+
outputRoot,
|
|
32470
|
+
global,
|
|
32471
|
+
normalizedPath,
|
|
32472
|
+
configuredPath
|
|
32473
|
+
});
|
|
32474
|
+
if ("rejectionMessage" in candidateResult) {
|
|
32475
|
+
logger?.warn(candidateResult.rejectionMessage);
|
|
32476
|
+
return fallback;
|
|
32477
|
+
}
|
|
32478
|
+
const candidatePath = candidateResult.path;
|
|
32479
|
+
const relativePath = (0, node_path.relative)((0, node_path.resolve)(outputRoot), candidatePath);
|
|
32480
|
+
if (relativePath === "" || splitPathSegments(normalizedPath).includes("..") || pathEscapesRoot(relativePath) || await resolvedPathEscapesRoot({
|
|
32481
|
+
rootPath: outputRoot,
|
|
32482
|
+
targetPath: candidatePath
|
|
32483
|
+
})) {
|
|
32484
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)}, which is outside the import scope or traverses a symbolic link. Importing ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32485
|
+
return fallback;
|
|
32486
|
+
}
|
|
32487
|
+
return {
|
|
32488
|
+
filePath: candidatePath,
|
|
32489
|
+
relativeDirPath: (0, node_path.dirname)(relativePath),
|
|
32490
|
+
relativeFilePath: (0, node_path.basename)(relativePath)
|
|
32491
|
+
};
|
|
32492
|
+
}
|
|
31935
32493
|
function disabledNamesOf(config) {
|
|
31936
32494
|
const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
|
|
31937
32495
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
@@ -32037,6 +32595,25 @@ function envVarMcpFileSpellings({ fileName }) {
|
|
|
32037
32595
|
return [`$HOME/${tail}`, `\${HOME}/${tail}`];
|
|
32038
32596
|
}
|
|
32039
32597
|
/**
|
|
32598
|
+
* Classify the existing `mcpConfigPath` without deciding how to report it.
|
|
32599
|
+
* Keep the known-file checks in this order: the generated file is valid in
|
|
32600
|
+
* either scope, while the documented default and environment-variable
|
|
32601
|
+
* spellings are global-only alternatives that need their own warnings.
|
|
32602
|
+
*/
|
|
32603
|
+
function classifyExistingPointer({ existing, global, outputRoot }) {
|
|
32604
|
+
if (existing === void 0) return { kind: "unset" };
|
|
32605
|
+
const normalized = typeof existing === "string" ? normalizeMcpConfigPathValue(existing) : void 0;
|
|
32606
|
+
const namesFile = (fileName) => normalized !== void 0 && mcpFileSpellings({
|
|
32607
|
+
fileName,
|
|
32608
|
+
global,
|
|
32609
|
+
outputRoot
|
|
32610
|
+
}).includes(normalized);
|
|
32611
|
+
if (namesFile("mcp.json")) return { kind: "already-generated" };
|
|
32612
|
+
if (global && namesFile(ROVODEV_ALTERNATE_MCP_FILE_NAME)) return { kind: "documented-default" };
|
|
32613
|
+
if (global && normalized !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalized)) return { kind: "env-var-spelling" };
|
|
32614
|
+
return { kind: "unrelated" };
|
|
32615
|
+
}
|
|
32616
|
+
/**
|
|
32040
32617
|
* Point `mcp.mcpConfigPath` at the `mcp.json` rulesync writes for this scope,
|
|
32041
32618
|
* and report whether the block gained a value it did not already carry.
|
|
32042
32619
|
*
|
|
@@ -32147,45 +32724,44 @@ function announcePointer({ global, logger }) {
|
|
|
32147
32724
|
async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outputRoot, logger }) {
|
|
32148
32725
|
const { pointer, configLabel, mcpLabel } = pointerLabels(global);
|
|
32149
32726
|
const existing = existingMcp.mcpConfigPath;
|
|
32150
|
-
const
|
|
32151
|
-
|
|
32152
|
-
fileName,
|
|
32727
|
+
const classification = classifyExistingPointer({
|
|
32728
|
+
existing,
|
|
32153
32729
|
global,
|
|
32154
32730
|
outputRoot
|
|
32155
|
-
})
|
|
32156
|
-
const pointsAtGeneratedFile = namesFile(ROVODEV_MCP_FILE_NAME);
|
|
32731
|
+
});
|
|
32157
32732
|
if (!hasLiveServers) {
|
|
32158
|
-
if (
|
|
32733
|
+
if (classification.kind === "already-generated") logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} points at ${mcpLabel}, which now has no enabled server. Rovo Dev reads MCP servers from that file and nowhere else, so ${global ? "Rovo Dev has" : "this project has"} no MCP servers at all until one targeting rovodev is added back — remove the mcp.mcpConfigPath line to fall back to ${global ? "Rovo Dev's own default" : "the global config"}.`);
|
|
32159
32734
|
return false;
|
|
32160
32735
|
}
|
|
32161
|
-
|
|
32162
|
-
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
|
|
32736
|
+
switch (classification.kind) {
|
|
32737
|
+
case "unset": {
|
|
32738
|
+
const displaced = global ? await describeDisplacedGlobalServers({ outputRoot }) : null;
|
|
32739
|
+
if (displaced !== null) {
|
|
32740
|
+
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.`);
|
|
32741
|
+
return false;
|
|
32742
|
+
}
|
|
32743
|
+
existingMcp.mcpConfigPath = pointer;
|
|
32744
|
+
announcePointer({
|
|
32745
|
+
global,
|
|
32746
|
+
logger
|
|
32747
|
+
});
|
|
32748
|
+
return true;
|
|
32166
32749
|
}
|
|
32167
|
-
|
|
32168
|
-
|
|
32169
|
-
|
|
32170
|
-
|
|
32171
|
-
|
|
32172
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
|
|
32176
|
-
|
|
32177
|
-
|
|
32178
|
-
|
|
32179
|
-
logger
|
|
32180
|
-
|
|
32181
|
-
return false;
|
|
32182
|
-
}
|
|
32183
|
-
if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
|
|
32184
|
-
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${quoteValueForWarning(existing)}. That names ${mcpLabel} only if Rovo Dev expands environment variables in this setting, which Atlassian does not document — if it does not, the path resolves literally and Rovo Dev reads no MCP servers at all. Write "${pointer}" instead, the form its own documented default uses.`);
|
|
32185
|
-
return false;
|
|
32750
|
+
case "already-generated": return false;
|
|
32751
|
+
case "documented-default":
|
|
32752
|
+
await warnAtDocumentedDefault({
|
|
32753
|
+
existing,
|
|
32754
|
+
outputRoot,
|
|
32755
|
+
logger
|
|
32756
|
+
});
|
|
32757
|
+
return false;
|
|
32758
|
+
case "env-var-spelling":
|
|
32759
|
+
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.`);
|
|
32760
|
+
return false;
|
|
32761
|
+
case "unrelated":
|
|
32762
|
+
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}".`);
|
|
32763
|
+
return false;
|
|
32186
32764
|
}
|
|
32187
|
-
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. Rovo Dev reads MCP servers from that path, so the generated ${mcpLabel} is unused until it is set to "${pointer}".`);
|
|
32188
|
-
return false;
|
|
32189
32765
|
}
|
|
32190
32766
|
/**
|
|
32191
32767
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
@@ -32227,14 +32803,20 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32227
32803
|
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32228
32804
|
};
|
|
32229
32805
|
}
|
|
32230
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32231
|
-
const
|
|
32232
|
-
const
|
|
32806
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
32807
|
+
const rovodevConfig = await readRovodevConfigYaml({ outputRoot });
|
|
32808
|
+
const paths = await resolveRovodevMcpImportPath({
|
|
32809
|
+
outputRoot,
|
|
32810
|
+
global,
|
|
32811
|
+
config: rovodevConfig,
|
|
32812
|
+
logger
|
|
32813
|
+
});
|
|
32814
|
+
const json = parseRovodevMcpJson(await readFileContentOrNull(paths.filePath) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
|
|
32233
32815
|
const newJson = {
|
|
32234
32816
|
...json,
|
|
32235
32817
|
mcpServers: json.mcpServers ?? {}
|
|
32236
32818
|
};
|
|
32237
|
-
const disabledNames = disabledNamesOf(
|
|
32819
|
+
const disabledNames = disabledNamesOf(rovodevConfig);
|
|
32238
32820
|
if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
|
|
32239
32821
|
const servers = newJson.mcpServers;
|
|
32240
32822
|
for (const name of disabledNames) {
|
|
@@ -32264,9 +32846,8 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32264
32846
|
} catch {
|
|
32265
32847
|
canWriteDisableToggle = false;
|
|
32266
32848
|
}
|
|
32267
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
32268
32849
|
const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
|
|
32269
|
-
const rawServer =
|
|
32850
|
+
const rawServer = rulesyncMcp.getRawMcpServer(name);
|
|
32270
32851
|
const record = {
|
|
32271
32852
|
...server,
|
|
32272
32853
|
...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
|
|
@@ -34254,6 +34835,518 @@ function convertAmpToRulesync({ disable, permissions }) {
|
|
|
34254
34835
|
return { permission };
|
|
34255
34836
|
}
|
|
34256
34837
|
//#endregion
|
|
34838
|
+
//#region src/utils/glob.ts
|
|
34839
|
+
/**
|
|
34840
|
+
* Convert a glob-like pattern into an anchored regex source string.
|
|
34841
|
+
*
|
|
34842
|
+
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34843
|
+
* every other regex metacharacter is escaped so it matches literally. The
|
|
34844
|
+
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34845
|
+
* name?" rather than "does this appear somewhere in it?".
|
|
34846
|
+
*
|
|
34847
|
+
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34848
|
+
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34849
|
+
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34850
|
+
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34851
|
+
* has to say what the tool would read rather than what a glob means. Use
|
|
34852
|
+
* `matchesGlob` for an actual comparison.
|
|
34853
|
+
*/
|
|
34854
|
+
function globToAnchoredRegexSource(glob) {
|
|
34855
|
+
let source = "";
|
|
34856
|
+
for (const char of glob) if (char === "*") source += ".*";
|
|
34857
|
+
else if (char === "?") source += ".";
|
|
34858
|
+
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34859
|
+
else source += char;
|
|
34860
|
+
return `^${source}$`;
|
|
34861
|
+
}
|
|
34862
|
+
/**
|
|
34863
|
+
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34864
|
+
* bracket is never closed — in which case it is an ordinary character.
|
|
34865
|
+
*/
|
|
34866
|
+
function parseGlobClass(characters, start) {
|
|
34867
|
+
let index = start;
|
|
34868
|
+
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34869
|
+
if (negated) index += 1;
|
|
34870
|
+
const members = /* @__PURE__ */ new Set();
|
|
34871
|
+
const ranges = [];
|
|
34872
|
+
let first = true;
|
|
34873
|
+
while (index < characters.length) {
|
|
34874
|
+
const character = characters[index] ?? "";
|
|
34875
|
+
if (character === "]" && !first) return {
|
|
34876
|
+
step: {
|
|
34877
|
+
kind: "class",
|
|
34878
|
+
negated,
|
|
34879
|
+
members,
|
|
34880
|
+
ranges
|
|
34881
|
+
},
|
|
34882
|
+
next: index + 1
|
|
34883
|
+
};
|
|
34884
|
+
first = false;
|
|
34885
|
+
const high = characters[index + 2];
|
|
34886
|
+
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34887
|
+
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34888
|
+
index += 3;
|
|
34889
|
+
continue;
|
|
34890
|
+
}
|
|
34891
|
+
members.add(character);
|
|
34892
|
+
index += 1;
|
|
34893
|
+
}
|
|
34894
|
+
}
|
|
34895
|
+
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34896
|
+
function parseGlob(glob) {
|
|
34897
|
+
const characters = [...glob];
|
|
34898
|
+
const steps = [];
|
|
34899
|
+
let index = 0;
|
|
34900
|
+
let bracketsAreClosed = true;
|
|
34901
|
+
while (index < characters.length) {
|
|
34902
|
+
const character = characters[index] ?? "";
|
|
34903
|
+
index += 1;
|
|
34904
|
+
if (character === "*") {
|
|
34905
|
+
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34906
|
+
continue;
|
|
34907
|
+
}
|
|
34908
|
+
if (character === "?") {
|
|
34909
|
+
steps.push({ kind: "any" });
|
|
34910
|
+
continue;
|
|
34911
|
+
}
|
|
34912
|
+
if (character === "[" && bracketsAreClosed) {
|
|
34913
|
+
const parsed = parseGlobClass(characters, index);
|
|
34914
|
+
if (parsed === void 0) bracketsAreClosed = false;
|
|
34915
|
+
else {
|
|
34916
|
+
steps.push(parsed.step);
|
|
34917
|
+
index = parsed.next;
|
|
34918
|
+
continue;
|
|
34919
|
+
}
|
|
34920
|
+
}
|
|
34921
|
+
steps.push({
|
|
34922
|
+
kind: "literal",
|
|
34923
|
+
character
|
|
34924
|
+
});
|
|
34925
|
+
}
|
|
34926
|
+
return steps;
|
|
34927
|
+
}
|
|
34928
|
+
function matchesGlobStep(step, character) {
|
|
34929
|
+
if (step.kind === "star") return false;
|
|
34930
|
+
if (step.kind === "any") return true;
|
|
34931
|
+
if (step.kind === "literal") return step.character === character;
|
|
34932
|
+
const code = character.codePointAt(0) ?? 0;
|
|
34933
|
+
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34934
|
+
return step.negated ? !admitted : admitted;
|
|
34935
|
+
}
|
|
34936
|
+
/** Whether two single-character steps can both match one same character. */
|
|
34937
|
+
function stepsShareACharacter(left, right) {
|
|
34938
|
+
if (left.kind === "any" || right.kind === "any") return true;
|
|
34939
|
+
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34940
|
+
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34941
|
+
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34942
|
+
return true;
|
|
34943
|
+
}
|
|
34944
|
+
/** Whether every step from `index` on can match the empty string. */
|
|
34945
|
+
function isAllStars(steps, index) {
|
|
34946
|
+
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34947
|
+
return true;
|
|
34948
|
+
}
|
|
34949
|
+
/**
|
|
34950
|
+
* The most work one intersection walk will do, counted in cells times the cost
|
|
34951
|
+
* of one. Past it the two patterns are reported as intersecting without being
|
|
34952
|
+
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34953
|
+
* enough to reach this is pathological rather than a command anybody typed.
|
|
34954
|
+
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34955
|
+
* closed.
|
|
34956
|
+
*/
|
|
34957
|
+
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34958
|
+
/**
|
|
34959
|
+
* The most work a whole run of comparisons will do. A caller holding R
|
|
34960
|
+
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34961
|
+
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34962
|
+
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34963
|
+
* that together take minutes. The shared budget is spent down across the run
|
|
34964
|
+
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34965
|
+
* again the direction that withholds an `allow` rather than writing one.
|
|
34966
|
+
*/
|
|
34967
|
+
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34968
|
+
/**
|
|
34969
|
+
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34970
|
+
* filling the two rows the table is held in, and collecting the answer.
|
|
34971
|
+
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34972
|
+
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34973
|
+
* short allow rules is n squared comparisons that never spend the budget down
|
|
34974
|
+
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34975
|
+
* walk length on the same exhaustible resource.
|
|
34976
|
+
*
|
|
34977
|
+
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34978
|
+
* which lowers how many pairs a run compares from around a million to about
|
|
34979
|
+
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34980
|
+
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34981
|
+
* answer exhaustion gives everywhere else.
|
|
34982
|
+
*/
|
|
34983
|
+
const INTERSECTION_PAIR_COST = 64;
|
|
34984
|
+
/**
|
|
34985
|
+
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34986
|
+
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34987
|
+
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34988
|
+
* it.
|
|
34989
|
+
*/
|
|
34990
|
+
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34991
|
+
return { remaining };
|
|
34992
|
+
}
|
|
34993
|
+
/**
|
|
34994
|
+
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34995
|
+
* the same pattern against a whole list parses it once and reuses the result.
|
|
34996
|
+
*/
|
|
34997
|
+
function parseGlobPattern(glob) {
|
|
34998
|
+
const steps = parseGlob(glob);
|
|
34999
|
+
return {
|
|
35000
|
+
steps,
|
|
35001
|
+
maxRanges: maxRangeCount(steps)
|
|
35002
|
+
};
|
|
35003
|
+
}
|
|
35004
|
+
/**
|
|
35005
|
+
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
35006
|
+
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
35007
|
+
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
35008
|
+
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
35009
|
+
* than on cells.
|
|
35010
|
+
*/
|
|
35011
|
+
function maxRangeCount(steps) {
|
|
35012
|
+
let most = 0;
|
|
35013
|
+
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
35014
|
+
return most;
|
|
35015
|
+
}
|
|
35016
|
+
/**
|
|
35017
|
+
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
35018
|
+
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
35019
|
+
* Once that budget is exhausted every further pair answers `true` without being
|
|
35020
|
+
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
35021
|
+
* safe side.
|
|
35022
|
+
*/
|
|
35023
|
+
function parsedGlobsIntersect(left, right, budget) {
|
|
35024
|
+
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
35025
|
+
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
35026
|
+
const cost = rows.length * columns.length * cellCost;
|
|
35027
|
+
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
35028
|
+
if (budget !== void 0) {
|
|
35029
|
+
const charge = cost + INTERSECTION_PAIR_COST;
|
|
35030
|
+
if (charge > budget.remaining) {
|
|
35031
|
+
budget.remaining = 0;
|
|
35032
|
+
return true;
|
|
35033
|
+
}
|
|
35034
|
+
budget.remaining -= charge;
|
|
35035
|
+
}
|
|
35036
|
+
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
35037
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
35038
|
+
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
35039
|
+
row[columns.length] = isAllStars(rows, i);
|
|
35040
|
+
for (let j = columns.length - 1; j >= 0; j--) {
|
|
35041
|
+
const rowStep = rows[i];
|
|
35042
|
+
const columnStep = columns[j];
|
|
35043
|
+
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
35044
|
+
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
35045
|
+
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
35046
|
+
continue;
|
|
35047
|
+
}
|
|
35048
|
+
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
35049
|
+
}
|
|
35050
|
+
next = row;
|
|
35051
|
+
}
|
|
35052
|
+
return next[0] ?? false;
|
|
35053
|
+
}
|
|
35054
|
+
//#endregion
|
|
35055
|
+
//#region src/features/permissions/shell-command-categories.ts
|
|
35056
|
+
/** The canonical category that names a shell command's permissions. */
|
|
35057
|
+
const SHELL_PERMISSION_CATEGORY = "bash";
|
|
35058
|
+
/**
|
|
35059
|
+
* Collect the canonical rules that govern shell commands, for the adapters
|
|
35060
|
+
* whose tool models commands and nothing else.
|
|
35061
|
+
*
|
|
35062
|
+
* The `bash` category contributes every rule. The all-tools `*` category
|
|
35063
|
+
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
35064
|
+
* written there covers shell commands too, and dropping it inverts the
|
|
35065
|
+
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
35066
|
+
* an adapter that reads only `bash` auto-approves the very command the file
|
|
35067
|
+
* denies.
|
|
35068
|
+
*
|
|
35069
|
+
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35070
|
+
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35071
|
+
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35072
|
+
* it in the permissive direction would grant something the author never said
|
|
35073
|
+
* about commands. Both directions therefore fail closed.
|
|
35074
|
+
*/
|
|
35075
|
+
function collectShellCommandRules(permission) {
|
|
35076
|
+
const rules = [];
|
|
35077
|
+
const foreignRestrictingCategories = [];
|
|
35078
|
+
const ignoredAllToolsAllowPatterns = [];
|
|
35079
|
+
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35080
|
+
if (category === "bash") {
|
|
35081
|
+
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35082
|
+
pattern,
|
|
35083
|
+
action,
|
|
35084
|
+
fromAllToolsCategory: false
|
|
35085
|
+
});
|
|
35086
|
+
continue;
|
|
35087
|
+
}
|
|
35088
|
+
if (category === "*") {
|
|
35089
|
+
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35090
|
+
if (action === "allow") {
|
|
35091
|
+
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35092
|
+
continue;
|
|
35093
|
+
}
|
|
35094
|
+
rules.push({
|
|
35095
|
+
pattern,
|
|
35096
|
+
action,
|
|
35097
|
+
fromAllToolsCategory: true
|
|
35098
|
+
});
|
|
35099
|
+
}
|
|
35100
|
+
continue;
|
|
35101
|
+
}
|
|
35102
|
+
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35103
|
+
}
|
|
35104
|
+
return {
|
|
35105
|
+
rules,
|
|
35106
|
+
foreignRestrictingCategories,
|
|
35107
|
+
ignoredAllToolsAllowPatterns
|
|
35108
|
+
};
|
|
35109
|
+
}
|
|
35110
|
+
/**
|
|
35111
|
+
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35112
|
+
* which restrictions it cannot write name some of the same commands? The
|
|
35113
|
+
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35114
|
+
* written — so a caller can report both the allow rules it withheld and the
|
|
35115
|
+
* restrictions that withheld nothing.
|
|
35116
|
+
*
|
|
35117
|
+
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35118
|
+
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35119
|
+
* asking whether any one command matches both. Width does not enter into it: an
|
|
35120
|
+
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35121
|
+
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35122
|
+
* on every `git ... --force` command even though neither pattern covers the
|
|
35123
|
+
* other's spelling. Comparing only identical spellings would let the most
|
|
35124
|
+
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35125
|
+
*
|
|
35126
|
+
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35127
|
+
* the walk for the commonest case.
|
|
35128
|
+
*
|
|
35129
|
+
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35130
|
+
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35131
|
+
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35132
|
+
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35133
|
+
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35134
|
+
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35135
|
+
* withholds an allow rather than writing one the config restricts — see
|
|
35136
|
+
* `warpCommandPatternToGlob`.
|
|
35137
|
+
*/
|
|
35138
|
+
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35139
|
+
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35140
|
+
pattern,
|
|
35141
|
+
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35142
|
+
}));
|
|
35143
|
+
return (allowPattern) => {
|
|
35144
|
+
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35145
|
+
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35146
|
+
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35147
|
+
};
|
|
35148
|
+
}
|
|
35149
|
+
/**
|
|
35150
|
+
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35151
|
+
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35152
|
+
*
|
|
35153
|
+
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35154
|
+
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35155
|
+
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35156
|
+
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35157
|
+
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35158
|
+
*
|
|
35159
|
+
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35160
|
+
* so overlapping no allow rule says nothing is wrong with it.
|
|
35161
|
+
*/
|
|
35162
|
+
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35163
|
+
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35164
|
+
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35165
|
+
return (0, es_toolkit.uniq)(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35166
|
+
}
|
|
35167
|
+
/**
|
|
35168
|
+
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35169
|
+
* commands with those two tiers and nothing else.
|
|
35170
|
+
*
|
|
35171
|
+
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35172
|
+
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35173
|
+
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35174
|
+
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35175
|
+
* about would answer the prompt the author wanted.
|
|
35176
|
+
*
|
|
35177
|
+
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35178
|
+
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35179
|
+
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35180
|
+
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35181
|
+
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35182
|
+
* cannot be written it withholds the allow rules it covers instead, which
|
|
35183
|
+
* restricts in the same direction without touching the denylist.
|
|
35184
|
+
*
|
|
35185
|
+
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35186
|
+
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35187
|
+
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35188
|
+
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35189
|
+
* even where it is written: a pattern under `*` need not name a command —
|
|
35190
|
+
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35191
|
+
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35192
|
+
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35193
|
+
* command pattern is reported; failing open would not be.
|
|
35194
|
+
*
|
|
35195
|
+
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35196
|
+
* patterns are not globs.
|
|
35197
|
+
*/
|
|
35198
|
+
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35199
|
+
const deny = [];
|
|
35200
|
+
const unwrittenDenyPatterns = [];
|
|
35201
|
+
const restrictions = [];
|
|
35202
|
+
const writtenAllToolsDenyPatterns = [];
|
|
35203
|
+
const allToolsAskPatterns = [];
|
|
35204
|
+
for (const rule of rules) {
|
|
35205
|
+
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35206
|
+
if (action === "allow") continue;
|
|
35207
|
+
if (action !== "deny") {
|
|
35208
|
+
restrictions.push(rule);
|
|
35209
|
+
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35210
|
+
continue;
|
|
35211
|
+
}
|
|
35212
|
+
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35213
|
+
deny.push(pattern);
|
|
35214
|
+
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35215
|
+
} else unwrittenDenyPatterns.push(pattern);
|
|
35216
|
+
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35217
|
+
}
|
|
35218
|
+
const budget = createIntersectionBudget();
|
|
35219
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35220
|
+
normalizePattern,
|
|
35221
|
+
budget
|
|
35222
|
+
});
|
|
35223
|
+
const allow = [];
|
|
35224
|
+
const shadowedAllowPatterns = [];
|
|
35225
|
+
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35226
|
+
for (const { pattern, action } of rules) {
|
|
35227
|
+
if (action !== "allow") continue;
|
|
35228
|
+
const shadowing = shadowingRestrictions(pattern);
|
|
35229
|
+
if (shadowing.length > 0) {
|
|
35230
|
+
shadowedAllowPatterns.push(pattern);
|
|
35231
|
+
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35232
|
+
continue;
|
|
35233
|
+
}
|
|
35234
|
+
allow.push(pattern);
|
|
35235
|
+
}
|
|
35236
|
+
return {
|
|
35237
|
+
allow,
|
|
35238
|
+
deny,
|
|
35239
|
+
shadowedAllowPatterns,
|
|
35240
|
+
unwrittenDenyPatterns,
|
|
35241
|
+
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35242
|
+
rules,
|
|
35243
|
+
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35244
|
+
withholdingPatterns
|
|
35245
|
+
}),
|
|
35246
|
+
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35247
|
+
rules,
|
|
35248
|
+
allToolsPatterns: allToolsAskPatterns,
|
|
35249
|
+
withholdingPatterns
|
|
35250
|
+
}),
|
|
35251
|
+
intersectionBudgetExhausted: budget.remaining === 0
|
|
35252
|
+
};
|
|
35253
|
+
}
|
|
35254
|
+
/**
|
|
35255
|
+
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35256
|
+
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35257
|
+
* dropped in one is worded the same way in all.
|
|
35258
|
+
*/
|
|
35259
|
+
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35260
|
+
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.`);
|
|
35261
|
+
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.`);
|
|
35262
|
+
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.`);
|
|
35263
|
+
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.`);
|
|
35264
|
+
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.`);
|
|
35265
|
+
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.`);
|
|
35266
|
+
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.`);
|
|
35267
|
+
}
|
|
35268
|
+
function resolveShellCommandState(permission, writesAllToolsDeny) {
|
|
35269
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permission);
|
|
35270
|
+
const partitioned = partitionCommandRules({
|
|
35271
|
+
rules,
|
|
35272
|
+
writesAllToolsDeny
|
|
35273
|
+
});
|
|
35274
|
+
return {
|
|
35275
|
+
allow: partitioned.allow,
|
|
35276
|
+
deny: partitioned.deny,
|
|
35277
|
+
bash: bashRulesHonoringAllTools(permission),
|
|
35278
|
+
foreignRestrictingCategories,
|
|
35279
|
+
ignoredAllToolsAllowPatterns,
|
|
35280
|
+
shadowedAllowPatterns: partitioned.shadowedAllowPatterns,
|
|
35281
|
+
unwrittenDenyPatterns: partitioned.unwrittenDenyPatterns,
|
|
35282
|
+
unenforcedAllToolsDenyPatterns: partitioned.unenforcedAllToolsDenyPatterns,
|
|
35283
|
+
unenforcedAllToolsAskPatterns: partitioned.unenforcedAllToolsAskPatterns,
|
|
35284
|
+
intersectionBudgetExhausted: partitioned.intersectionBudgetExhausted
|
|
35285
|
+
};
|
|
35286
|
+
}
|
|
35287
|
+
/**
|
|
35288
|
+
* Collect shell-command allow/deny lists the way the command-only adapters do,
|
|
35289
|
+
* and report every restriction the surface cannot carry.
|
|
35290
|
+
*/
|
|
35291
|
+
function resolveShellCommandLists({ permission, writesAllToolsDeny, toolLabel, surfaceLabel, logger }) {
|
|
35292
|
+
const resolved = resolveShellCommandState(permission, writesAllToolsDeny);
|
|
35293
|
+
warnAboutUnwrittenCommandRules({
|
|
35294
|
+
toolLabel,
|
|
35295
|
+
surfaceLabel,
|
|
35296
|
+
foreignRestrictingCategories: resolved.foreignRestrictingCategories,
|
|
35297
|
+
shadowedAllowPatterns: resolved.shadowedAllowPatterns,
|
|
35298
|
+
unwrittenDenyPatterns: resolved.unwrittenDenyPatterns,
|
|
35299
|
+
unenforcedAllToolsDenyPatterns: resolved.unenforcedAllToolsDenyPatterns,
|
|
35300
|
+
unenforcedAllToolsAskPatterns: resolved.unenforcedAllToolsAskPatterns,
|
|
35301
|
+
ignoredAllToolsAllowPatterns: resolved.ignoredAllToolsAllowPatterns,
|
|
35302
|
+
intersectionBudgetExhausted: resolved.intersectionBudgetExhausted,
|
|
35303
|
+
logger
|
|
35304
|
+
});
|
|
35305
|
+
return {
|
|
35306
|
+
allow: resolved.allow,
|
|
35307
|
+
deny: resolved.deny,
|
|
35308
|
+
bash: resolved.bash
|
|
35309
|
+
};
|
|
35310
|
+
}
|
|
35311
|
+
/**
|
|
35312
|
+
* The `bash` category after all-tools `*` restrictions have been applied. A
|
|
35313
|
+
* `deny`/`ask` written under `*` covers shell commands too, so a bash `allow`
|
|
35314
|
+
* it overlaps is withheld, a `*` deny is copied in, and a `*` ask is copied in
|
|
35315
|
+
* wherever `bash` says nothing about that exact pattern yet — otherwise it
|
|
35316
|
+
* would vanish from the resolved category entirely rather than falling back to
|
|
35317
|
+
* a tier that still prompts. An existing `bash` entry for the same pattern is
|
|
35318
|
+
* never downgraded by a `*` ask (a bash `allow` was already dropped above, and
|
|
35319
|
+
* a bash `deny`/`ask` there is at least as strict already).
|
|
35320
|
+
*/
|
|
35321
|
+
function bashRulesHonoringAllTools(permission) {
|
|
35322
|
+
const { rules } = collectShellCommandRules(permission);
|
|
35323
|
+
const allToolsRestrictions = rules.filter(({ fromAllToolsCategory }) => fromAllToolsCategory);
|
|
35324
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(allToolsRestrictions);
|
|
35325
|
+
const bash = { ...permission.bash };
|
|
35326
|
+
for (const [pattern, action] of Object.entries(bash)) if (action === "allow" && shadowingRestrictions(pattern).length > 0) delete bash[pattern];
|
|
35327
|
+
for (const { pattern, action } of allToolsRestrictions) {
|
|
35328
|
+
if (isPrototypePollutionKey(pattern)) continue;
|
|
35329
|
+
if (action === "deny") {
|
|
35330
|
+
if (bash[pattern] !== "ask") bash[pattern] = "deny";
|
|
35331
|
+
continue;
|
|
35332
|
+
}
|
|
35333
|
+
if (bash[pattern] === void 0) bash[pattern] = "ask";
|
|
35334
|
+
}
|
|
35335
|
+
return bash;
|
|
35336
|
+
}
|
|
35337
|
+
/**
|
|
35338
|
+
* Return a permission block whose `bash` category honors all-tools `*`
|
|
35339
|
+
* restrictions. Other categories are unchanged, so adapters that already model
|
|
35340
|
+
* `*` keep doing so.
|
|
35341
|
+
*/
|
|
35342
|
+
function honorAllToolsOnBash(permission) {
|
|
35343
|
+
if (permission.bash === void 0) return permission;
|
|
35344
|
+
return {
|
|
35345
|
+
...permission,
|
|
35346
|
+
bash: bashRulesHonoringAllTools(permission)
|
|
35347
|
+
};
|
|
35348
|
+
}
|
|
35349
|
+
//#endregion
|
|
34257
35350
|
//#region src/features/permissions/antigravity-cli-permissions.ts
|
|
34258
35351
|
/**
|
|
34259
35352
|
* Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
|
|
@@ -34501,7 +35594,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
|
|
|
34501
35594
|
const allow = [];
|
|
34502
35595
|
const ask = [];
|
|
34503
35596
|
const deny = [];
|
|
34504
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35597
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34505
35598
|
const cliToolName = toAntigravityCliToolName(category);
|
|
34506
35599
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
34507
35600
|
const entry = buildPermissionEntry$1(cliToolName, pattern);
|
|
@@ -34712,7 +35805,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
|
|
|
34712
35805
|
const allow = [];
|
|
34713
35806
|
const ask = [];
|
|
34714
35807
|
const deny = [];
|
|
34715
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35808
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34716
35809
|
const action = toIdeAction(category);
|
|
34717
35810
|
for (const [pattern, permissionAction] of Object.entries(rules)) {
|
|
34718
35811
|
const entry = buildPermissionEntry(action, pattern);
|
|
@@ -34752,223 +35845,6 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
34752
35845
|
return { permission };
|
|
34753
35846
|
}
|
|
34754
35847
|
//#endregion
|
|
34755
|
-
//#region src/utils/glob.ts
|
|
34756
|
-
/**
|
|
34757
|
-
* Convert a glob-like pattern into an anchored regex source string.
|
|
34758
|
-
*
|
|
34759
|
-
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34760
|
-
* every other regex metacharacter is escaped so it matches literally. The
|
|
34761
|
-
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34762
|
-
* name?" rather than "does this appear somewhere in it?".
|
|
34763
|
-
*
|
|
34764
|
-
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34765
|
-
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34766
|
-
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34767
|
-
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34768
|
-
* has to say what the tool would read rather than what a glob means. Use
|
|
34769
|
-
* `matchesGlob` for an actual comparison.
|
|
34770
|
-
*/
|
|
34771
|
-
function globToAnchoredRegexSource(glob) {
|
|
34772
|
-
let source = "";
|
|
34773
|
-
for (const char of glob) if (char === "*") source += ".*";
|
|
34774
|
-
else if (char === "?") source += ".";
|
|
34775
|
-
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34776
|
-
else source += char;
|
|
34777
|
-
return `^${source}$`;
|
|
34778
|
-
}
|
|
34779
|
-
/**
|
|
34780
|
-
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34781
|
-
* bracket is never closed — in which case it is an ordinary character.
|
|
34782
|
-
*/
|
|
34783
|
-
function parseGlobClass(characters, start) {
|
|
34784
|
-
let index = start;
|
|
34785
|
-
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34786
|
-
if (negated) index += 1;
|
|
34787
|
-
const members = /* @__PURE__ */ new Set();
|
|
34788
|
-
const ranges = [];
|
|
34789
|
-
let first = true;
|
|
34790
|
-
while (index < characters.length) {
|
|
34791
|
-
const character = characters[index] ?? "";
|
|
34792
|
-
if (character === "]" && !first) return {
|
|
34793
|
-
step: {
|
|
34794
|
-
kind: "class",
|
|
34795
|
-
negated,
|
|
34796
|
-
members,
|
|
34797
|
-
ranges
|
|
34798
|
-
},
|
|
34799
|
-
next: index + 1
|
|
34800
|
-
};
|
|
34801
|
-
first = false;
|
|
34802
|
-
const high = characters[index + 2];
|
|
34803
|
-
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34804
|
-
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34805
|
-
index += 3;
|
|
34806
|
-
continue;
|
|
34807
|
-
}
|
|
34808
|
-
members.add(character);
|
|
34809
|
-
index += 1;
|
|
34810
|
-
}
|
|
34811
|
-
}
|
|
34812
|
-
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34813
|
-
function parseGlob(glob) {
|
|
34814
|
-
const characters = [...glob];
|
|
34815
|
-
const steps = [];
|
|
34816
|
-
let index = 0;
|
|
34817
|
-
let bracketsAreClosed = true;
|
|
34818
|
-
while (index < characters.length) {
|
|
34819
|
-
const character = characters[index] ?? "";
|
|
34820
|
-
index += 1;
|
|
34821
|
-
if (character === "*") {
|
|
34822
|
-
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34823
|
-
continue;
|
|
34824
|
-
}
|
|
34825
|
-
if (character === "?") {
|
|
34826
|
-
steps.push({ kind: "any" });
|
|
34827
|
-
continue;
|
|
34828
|
-
}
|
|
34829
|
-
if (character === "[" && bracketsAreClosed) {
|
|
34830
|
-
const parsed = parseGlobClass(characters, index);
|
|
34831
|
-
if (parsed === void 0) bracketsAreClosed = false;
|
|
34832
|
-
else {
|
|
34833
|
-
steps.push(parsed.step);
|
|
34834
|
-
index = parsed.next;
|
|
34835
|
-
continue;
|
|
34836
|
-
}
|
|
34837
|
-
}
|
|
34838
|
-
steps.push({
|
|
34839
|
-
kind: "literal",
|
|
34840
|
-
character
|
|
34841
|
-
});
|
|
34842
|
-
}
|
|
34843
|
-
return steps;
|
|
34844
|
-
}
|
|
34845
|
-
function matchesGlobStep(step, character) {
|
|
34846
|
-
if (step.kind === "star") return false;
|
|
34847
|
-
if (step.kind === "any") return true;
|
|
34848
|
-
if (step.kind === "literal") return step.character === character;
|
|
34849
|
-
const code = character.codePointAt(0) ?? 0;
|
|
34850
|
-
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34851
|
-
return step.negated ? !admitted : admitted;
|
|
34852
|
-
}
|
|
34853
|
-
/** Whether two single-character steps can both match one same character. */
|
|
34854
|
-
function stepsShareACharacter(left, right) {
|
|
34855
|
-
if (left.kind === "any" || right.kind === "any") return true;
|
|
34856
|
-
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34857
|
-
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34858
|
-
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34859
|
-
return true;
|
|
34860
|
-
}
|
|
34861
|
-
/** Whether every step from `index` on can match the empty string. */
|
|
34862
|
-
function isAllStars(steps, index) {
|
|
34863
|
-
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34864
|
-
return true;
|
|
34865
|
-
}
|
|
34866
|
-
/**
|
|
34867
|
-
* The most work one intersection walk will do, counted in cells times the cost
|
|
34868
|
-
* of one. Past it the two patterns are reported as intersecting without being
|
|
34869
|
-
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34870
|
-
* enough to reach this is pathological rather than a command anybody typed.
|
|
34871
|
-
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34872
|
-
* closed.
|
|
34873
|
-
*/
|
|
34874
|
-
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34875
|
-
/**
|
|
34876
|
-
* The most work a whole run of comparisons will do. A caller holding R
|
|
34877
|
-
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34878
|
-
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34879
|
-
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34880
|
-
* that together take minutes. The shared budget is spent down across the run
|
|
34881
|
-
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34882
|
-
* again the direction that withholds an `allow` rather than writing one.
|
|
34883
|
-
*/
|
|
34884
|
-
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34885
|
-
/**
|
|
34886
|
-
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34887
|
-
* filling the two rows the table is held in, and collecting the answer.
|
|
34888
|
-
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34889
|
-
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34890
|
-
* short allow rules is n squared comparisons that never spend the budget down
|
|
34891
|
-
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34892
|
-
* walk length on the same exhaustible resource.
|
|
34893
|
-
*
|
|
34894
|
-
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34895
|
-
* which lowers how many pairs a run compares from around a million to about
|
|
34896
|
-
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34897
|
-
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34898
|
-
* answer exhaustion gives everywhere else.
|
|
34899
|
-
*/
|
|
34900
|
-
const INTERSECTION_PAIR_COST = 64;
|
|
34901
|
-
/**
|
|
34902
|
-
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34903
|
-
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34904
|
-
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34905
|
-
* it.
|
|
34906
|
-
*/
|
|
34907
|
-
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34908
|
-
return { remaining };
|
|
34909
|
-
}
|
|
34910
|
-
/**
|
|
34911
|
-
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34912
|
-
* the same pattern against a whole list parses it once and reuses the result.
|
|
34913
|
-
*/
|
|
34914
|
-
function parseGlobPattern(glob) {
|
|
34915
|
-
const steps = parseGlob(glob);
|
|
34916
|
-
return {
|
|
34917
|
-
steps,
|
|
34918
|
-
maxRanges: maxRangeCount(steps)
|
|
34919
|
-
};
|
|
34920
|
-
}
|
|
34921
|
-
/**
|
|
34922
|
-
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34923
|
-
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34924
|
-
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34925
|
-
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34926
|
-
* than on cells.
|
|
34927
|
-
*/
|
|
34928
|
-
function maxRangeCount(steps) {
|
|
34929
|
-
let most = 0;
|
|
34930
|
-
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34931
|
-
return most;
|
|
34932
|
-
}
|
|
34933
|
-
/**
|
|
34934
|
-
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34935
|
-
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34936
|
-
* Once that budget is exhausted every further pair answers `true` without being
|
|
34937
|
-
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34938
|
-
* safe side.
|
|
34939
|
-
*/
|
|
34940
|
-
function parsedGlobsIntersect(left, right, budget) {
|
|
34941
|
-
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
34942
|
-
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
34943
|
-
const cost = rows.length * columns.length * cellCost;
|
|
34944
|
-
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
34945
|
-
if (budget !== void 0) {
|
|
34946
|
-
const charge = cost + INTERSECTION_PAIR_COST;
|
|
34947
|
-
if (charge > budget.remaining) {
|
|
34948
|
-
budget.remaining = 0;
|
|
34949
|
-
return true;
|
|
34950
|
-
}
|
|
34951
|
-
budget.remaining -= charge;
|
|
34952
|
-
}
|
|
34953
|
-
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
34954
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
34955
|
-
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
34956
|
-
row[columns.length] = isAllStars(rows, i);
|
|
34957
|
-
for (let j = columns.length - 1; j >= 0; j--) {
|
|
34958
|
-
const rowStep = rows[i];
|
|
34959
|
-
const columnStep = columns[j];
|
|
34960
|
-
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
34961
|
-
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
34962
|
-
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
34963
|
-
continue;
|
|
34964
|
-
}
|
|
34965
|
-
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
34966
|
-
}
|
|
34967
|
-
next = row;
|
|
34968
|
-
}
|
|
34969
|
-
return next[0] ?? false;
|
|
34970
|
-
}
|
|
34971
|
-
//#endregion
|
|
34972
35848
|
//#region src/features/permissions/augmentcode-permissions.ts
|
|
34973
35849
|
const moduleLogger$2 = fallbackLogger;
|
|
34974
35850
|
zod_mini.z.enum([
|
|
@@ -35220,6 +36096,7 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35220
36096
|
const basicExistingEntries = existingEntries.filter((entry) => !isSpecialEntry(entry));
|
|
35221
36097
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
35222
36098
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
36099
|
+
if (entry.toolName === "*") return false;
|
|
35223
36100
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(entry.toolName)) return true;
|
|
35224
36101
|
if (entry.permission.type === "deny") {
|
|
35225
36102
|
const key = `${entry.toolName}|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
@@ -35297,7 +36174,18 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35297
36174
|
};
|
|
35298
36175
|
function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
35299
36176
|
const entries = [];
|
|
35300
|
-
|
|
36177
|
+
const resolvedBashRules = bashRulesHonoringAllTools(config.permission);
|
|
36178
|
+
const permission = config.permission.bash !== void 0 || Object.keys(resolvedBashRules).length > 0 ? {
|
|
36179
|
+
...config.permission,
|
|
36180
|
+
bash: resolvedBashRules
|
|
36181
|
+
} : config.permission;
|
|
36182
|
+
const allToolsFailClosedType = computeAllToolsFailClosedType(config.permission["*"]);
|
|
36183
|
+
const categoriesWithOwnEntries = /* @__PURE__ */ new Set();
|
|
36184
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
36185
|
+
if (category === "*") {
|
|
36186
|
+
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.");
|
|
36187
|
+
continue;
|
|
36188
|
+
}
|
|
35301
36189
|
const augmentToolName = toAugmentToolName(category);
|
|
35302
36190
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(augmentToolName) && augmentToolName === category) logger?.warn(`AugmentCode permissions: passing through unknown tool category '${category}' as toolName.`);
|
|
35303
36191
|
if (augmentToolName === "launch-process") {
|
|
@@ -35322,19 +36210,50 @@ function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
|
35322
36210
|
toolName: augmentToolName,
|
|
35323
36211
|
permission: { type: "deny" }
|
|
35324
36212
|
});
|
|
36213
|
+
categoriesWithOwnEntries.add(category);
|
|
35325
36214
|
continue;
|
|
35326
36215
|
}
|
|
35327
36216
|
const droppedPatterns = [];
|
|
35328
|
-
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*")
|
|
35329
|
-
|
|
35330
|
-
|
|
35331
|
-
|
|
35332
|
-
|
|
36217
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") {
|
|
36218
|
+
entries.push({
|
|
36219
|
+
toolName: augmentToolName,
|
|
36220
|
+
permission: { type: actionToAugmentType(action) }
|
|
36221
|
+
});
|
|
36222
|
+
categoriesWithOwnEntries.add(category);
|
|
36223
|
+
} else droppedPatterns.push(pattern);
|
|
35333
36224
|
if (droppedPatterns.length > 0) logger?.warn(`AugmentCode permissions: dropping non-wildcard patterns for category '${category}' (${droppedPatterns.join(", ")}); AugmentCode does not document a per-input matcher for this tool. Use a 'deny' rule with pattern '*' if you need to block this tool entirely.`);
|
|
35334
36225
|
}
|
|
36226
|
+
entries.push(...synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType));
|
|
35335
36227
|
return entries;
|
|
35336
36228
|
}
|
|
35337
36229
|
/**
|
|
36230
|
+
* The strictest action the all-tools `*` category imposes, for tools with no per-input matcher to
|
|
36231
|
+
* narrow it onto (see {@link synthesizeManagedToolFallbackEntries}). `deny` wins over `ask`,
|
|
36232
|
+
* and an all-tools `allow` never forces an entry — there is nothing to fail closed on.
|
|
36233
|
+
*/
|
|
36234
|
+
function computeAllToolsFailClosedType(allToolsRules) {
|
|
36235
|
+
if (!allToolsRules) return void 0;
|
|
36236
|
+
const actions = Object.values(allToolsRules);
|
|
36237
|
+
if (actions.some((action) => action === "deny")) return "deny";
|
|
36238
|
+
if (actions.some((action) => action === "ask")) return "ask-user";
|
|
36239
|
+
}
|
|
36240
|
+
/**
|
|
36241
|
+
* Extend the same fail-closed treatment `bash` gets (via {@link bashRulesHonoringAllTools}) to the
|
|
36242
|
+
* other managed tools: one that produced no entries of its own above must not fall back to
|
|
36243
|
+
* AugmentCode's own default just because it has no per-input matcher to narrow the all-tools
|
|
36244
|
+
* restriction onto. A category can be stated yet still emit nothing (e.g. only non-`*` allow/ask
|
|
36245
|
+
* patterns, dropped with a warning), so this checks emitted entries rather than whether the
|
|
36246
|
+
* category was merely present in the source config. A tool whose own rules did emit entries is
|
|
36247
|
+
* left untouched here.
|
|
36248
|
+
*/
|
|
36249
|
+
function synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType) {
|
|
36250
|
+
if (allToolsFailClosedType === void 0) return [];
|
|
36251
|
+
return Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).filter(([canonicalName]) => canonicalName !== "bash" && !categoriesWithOwnEntries.has(canonicalName)).map(([, augmentToolName]) => ({
|
|
36252
|
+
toolName: augmentToolName,
|
|
36253
|
+
permission: { type: allToolsFailClosedType }
|
|
36254
|
+
}));
|
|
36255
|
+
}
|
|
36256
|
+
/**
|
|
35338
36257
|
* Sort AugmentCode tool-permission entries to make the `first-match-wins` semantics safe and predictable.
|
|
35339
36258
|
*
|
|
35340
36259
|
* Augment evaluates `toolPermissions` top-to-bottom and stops at the first match. To prevent a
|
|
@@ -35429,216 +36348,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
35429
36348
|
}
|
|
35430
36349
|
return { permission };
|
|
35431
36350
|
}
|
|
35432
|
-
/**
|
|
35433
|
-
* Collect the canonical rules that govern shell commands, for the adapters
|
|
35434
|
-
* whose tool models commands and nothing else.
|
|
35435
|
-
*
|
|
35436
|
-
* The `bash` category contributes every rule. The all-tools `*` category
|
|
35437
|
-
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
35438
|
-
* written there covers shell commands too, and dropping it inverts the
|
|
35439
|
-
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
35440
|
-
* an adapter that reads only `bash` auto-approves the very command the file
|
|
35441
|
-
* denies.
|
|
35442
|
-
*
|
|
35443
|
-
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35444
|
-
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35445
|
-
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35446
|
-
* it in the permissive direction would grant something the author never said
|
|
35447
|
-
* about commands. Both directions therefore fail closed.
|
|
35448
|
-
*/
|
|
35449
|
-
function collectShellCommandRules(permission) {
|
|
35450
|
-
const rules = [];
|
|
35451
|
-
const foreignRestrictingCategories = [];
|
|
35452
|
-
const ignoredAllToolsAllowPatterns = [];
|
|
35453
|
-
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35454
|
-
if (category === "bash") {
|
|
35455
|
-
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35456
|
-
pattern,
|
|
35457
|
-
action,
|
|
35458
|
-
fromAllToolsCategory: false
|
|
35459
|
-
});
|
|
35460
|
-
continue;
|
|
35461
|
-
}
|
|
35462
|
-
if (category === "*") {
|
|
35463
|
-
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35464
|
-
if (action === "allow") {
|
|
35465
|
-
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35466
|
-
continue;
|
|
35467
|
-
}
|
|
35468
|
-
rules.push({
|
|
35469
|
-
pattern,
|
|
35470
|
-
action,
|
|
35471
|
-
fromAllToolsCategory: true
|
|
35472
|
-
});
|
|
35473
|
-
}
|
|
35474
|
-
continue;
|
|
35475
|
-
}
|
|
35476
|
-
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35477
|
-
}
|
|
35478
|
-
return {
|
|
35479
|
-
rules,
|
|
35480
|
-
foreignRestrictingCategories,
|
|
35481
|
-
ignoredAllToolsAllowPatterns
|
|
35482
|
-
};
|
|
35483
|
-
}
|
|
35484
|
-
/**
|
|
35485
|
-
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35486
|
-
* which restrictions it cannot write name some of the same commands? The
|
|
35487
|
-
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35488
|
-
* written — so a caller can report both the allow rules it withheld and the
|
|
35489
|
-
* restrictions that withheld nothing.
|
|
35490
|
-
*
|
|
35491
|
-
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35492
|
-
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35493
|
-
* asking whether any one command matches both. Width does not enter into it: an
|
|
35494
|
-
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35495
|
-
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35496
|
-
* on every `git ... --force` command even though neither pattern covers the
|
|
35497
|
-
* other's spelling. Comparing only identical spellings would let the most
|
|
35498
|
-
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35499
|
-
*
|
|
35500
|
-
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35501
|
-
* the walk for the commonest case.
|
|
35502
|
-
*
|
|
35503
|
-
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35504
|
-
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35505
|
-
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35506
|
-
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35507
|
-
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35508
|
-
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35509
|
-
* withholds an allow rather than writing one the config restricts — see
|
|
35510
|
-
* `warpCommandPatternToGlob`.
|
|
35511
|
-
*/
|
|
35512
|
-
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35513
|
-
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35514
|
-
pattern,
|
|
35515
|
-
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35516
|
-
}));
|
|
35517
|
-
return (allowPattern) => {
|
|
35518
|
-
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35519
|
-
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35520
|
-
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35521
|
-
};
|
|
35522
|
-
}
|
|
35523
|
-
/**
|
|
35524
|
-
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35525
|
-
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35526
|
-
*
|
|
35527
|
-
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35528
|
-
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35529
|
-
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35530
|
-
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35531
|
-
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35532
|
-
*
|
|
35533
|
-
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35534
|
-
* so overlapping no allow rule says nothing is wrong with it.
|
|
35535
|
-
*/
|
|
35536
|
-
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35537
|
-
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35538
|
-
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35539
|
-
return (0, es_toolkit.uniq)(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35540
|
-
}
|
|
35541
|
-
/**
|
|
35542
|
-
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35543
|
-
* commands with those two tiers and nothing else.
|
|
35544
|
-
*
|
|
35545
|
-
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35546
|
-
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35547
|
-
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35548
|
-
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35549
|
-
* about would answer the prompt the author wanted.
|
|
35550
|
-
*
|
|
35551
|
-
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35552
|
-
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35553
|
-
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35554
|
-
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35555
|
-
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35556
|
-
* cannot be written it withholds the allow rules it covers instead, which
|
|
35557
|
-
* restricts in the same direction without touching the denylist.
|
|
35558
|
-
*
|
|
35559
|
-
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35560
|
-
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35561
|
-
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35562
|
-
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35563
|
-
* even where it is written: a pattern under `*` need not name a command —
|
|
35564
|
-
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35565
|
-
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35566
|
-
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35567
|
-
* command pattern is reported; failing open would not be.
|
|
35568
|
-
*
|
|
35569
|
-
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35570
|
-
* patterns are not globs.
|
|
35571
|
-
*/
|
|
35572
|
-
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35573
|
-
const deny = [];
|
|
35574
|
-
const unwrittenDenyPatterns = [];
|
|
35575
|
-
const restrictions = [];
|
|
35576
|
-
const writtenAllToolsDenyPatterns = [];
|
|
35577
|
-
const allToolsAskPatterns = [];
|
|
35578
|
-
for (const rule of rules) {
|
|
35579
|
-
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35580
|
-
if (action === "allow") continue;
|
|
35581
|
-
if (action !== "deny") {
|
|
35582
|
-
restrictions.push(rule);
|
|
35583
|
-
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35584
|
-
continue;
|
|
35585
|
-
}
|
|
35586
|
-
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35587
|
-
deny.push(pattern);
|
|
35588
|
-
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35589
|
-
} else unwrittenDenyPatterns.push(pattern);
|
|
35590
|
-
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35591
|
-
}
|
|
35592
|
-
const budget = createIntersectionBudget();
|
|
35593
|
-
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35594
|
-
normalizePattern,
|
|
35595
|
-
budget
|
|
35596
|
-
});
|
|
35597
|
-
const allow = [];
|
|
35598
|
-
const shadowedAllowPatterns = [];
|
|
35599
|
-
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35600
|
-
for (const { pattern, action } of rules) {
|
|
35601
|
-
if (action !== "allow") continue;
|
|
35602
|
-
const shadowing = shadowingRestrictions(pattern);
|
|
35603
|
-
if (shadowing.length > 0) {
|
|
35604
|
-
shadowedAllowPatterns.push(pattern);
|
|
35605
|
-
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35606
|
-
continue;
|
|
35607
|
-
}
|
|
35608
|
-
allow.push(pattern);
|
|
35609
|
-
}
|
|
35610
|
-
return {
|
|
35611
|
-
allow,
|
|
35612
|
-
deny,
|
|
35613
|
-
shadowedAllowPatterns,
|
|
35614
|
-
unwrittenDenyPatterns,
|
|
35615
|
-
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35616
|
-
rules,
|
|
35617
|
-
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35618
|
-
withholdingPatterns
|
|
35619
|
-
}),
|
|
35620
|
-
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35621
|
-
rules,
|
|
35622
|
-
allToolsPatterns: allToolsAskPatterns,
|
|
35623
|
-
withholdingPatterns
|
|
35624
|
-
}),
|
|
35625
|
-
intersectionBudgetExhausted: budget.remaining === 0
|
|
35626
|
-
};
|
|
35627
|
-
}
|
|
35628
|
-
/**
|
|
35629
|
-
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35630
|
-
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35631
|
-
* dropped in one is worded the same way in all.
|
|
35632
|
-
*/
|
|
35633
|
-
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35634
|
-
if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
|
|
35635
|
-
for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
|
|
35636
|
-
if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
|
|
35637
|
-
if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
|
|
35638
|
-
if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
|
|
35639
|
-
if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
|
|
35640
|
-
if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
|
|
35641
|
-
}
|
|
35642
36351
|
//#endregion
|
|
35643
36352
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
35644
36353
|
/**
|
|
@@ -37400,7 +38109,7 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
|
|
|
37400
38109
|
return merged;
|
|
37401
38110
|
}
|
|
37402
38111
|
function buildCodexBashRulesContent(config) {
|
|
37403
|
-
const bashRules = config.permission
|
|
38112
|
+
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
37404
38113
|
const entries = Object.entries(bashRules);
|
|
37405
38114
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
37406
38115
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
@@ -38091,7 +38800,7 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
38091
38800
|
function convertRulesyncToCursorPermissions(config, logger) {
|
|
38092
38801
|
const allow = [];
|
|
38093
38802
|
const deny = [];
|
|
38094
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
38803
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38095
38804
|
const cursorType = toCursorType(category);
|
|
38096
38805
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38097
38806
|
const entry = buildCursorPermissionEntry(cursorType, toCursorPattern(category, pattern));
|
|
@@ -38910,7 +39619,7 @@ function convertRulesyncToDevinPermissions(config) {
|
|
|
38910
39619
|
const allow = [];
|
|
38911
39620
|
const ask = [];
|
|
38912
39621
|
const deny = [];
|
|
38913
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
39622
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38914
39623
|
const scope = toDevinScope(category);
|
|
38915
39624
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38916
39625
|
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
@@ -39198,6 +39907,19 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
39198
39907
|
isDeletable() {
|
|
39199
39908
|
return false;
|
|
39200
39909
|
}
|
|
39910
|
+
/**
|
|
39911
|
+
* `permission.yaml` is Goose's file, not one rulesync owns: rulesync merges
|
|
39912
|
+
* into it when it exists but has no business bringing it into existence to
|
|
39913
|
+
* hold nothing. When no rule maps, the `user` block holds three empty lists,
|
|
39914
|
+
* which would otherwise be written as a fresh permission.yaml that says
|
|
39915
|
+
* nothing — an absent file and empty lists both mean "no user override, so
|
|
39916
|
+
* Goose decides on its own". An existing file is still rewritten as before,
|
|
39917
|
+
* so user content is never dropped — the skip only applies when there is no
|
|
39918
|
+
* file yet.
|
|
39919
|
+
*/
|
|
39920
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
39921
|
+
return true;
|
|
39922
|
+
}
|
|
39201
39923
|
static getSettablePaths(_options) {
|
|
39202
39924
|
return {
|
|
39203
39925
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
@@ -39282,7 +40004,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
39282
40004
|
never_allow: []
|
|
39283
40005
|
};
|
|
39284
40006
|
const assigned = /* @__PURE__ */ new Map();
|
|
39285
|
-
const orderedEntries = Object.entries(config.permission).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
40007
|
+
const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39286
40008
|
for (const [category, rules] of orderedEntries) {
|
|
39287
40009
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
39288
40010
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -39614,7 +40336,7 @@ function unmanagedEntries(existingPermission, key) {
|
|
|
39614
40336
|
*/
|
|
39615
40337
|
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
39616
40338
|
const ranked = /* @__PURE__ */ new Map();
|
|
39617
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
40339
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
39618
40340
|
const entry = buildGrokEntry(category, pattern);
|
|
39619
40341
|
if (entry === null) {
|
|
39620
40342
|
if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
|
|
@@ -39703,6 +40425,8 @@ function deriveGrokPermissionMode(config) {
|
|
|
39703
40425
|
function patternsByAction(category, action) {
|
|
39704
40426
|
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
39705
40427
|
}
|
|
40428
|
+
/** The canonical category whose `deny` rules feed `security.website_blocklist`. */
|
|
40429
|
+
const WEBFETCH_PERMISSION_CATEGORY = "webfetch";
|
|
39706
40430
|
function clonePermissionBlock(permission) {
|
|
39707
40431
|
return Object.fromEntries(Object.entries(permission).map(([category, rules]) => [category, { ...rules }]));
|
|
39708
40432
|
}
|
|
@@ -39715,18 +40439,39 @@ function ensureCategory(permission, category) {
|
|
|
39715
40439
|
function removeEmptyCategories(permission) {
|
|
39716
40440
|
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
39717
40441
|
}
|
|
40442
|
+
/**
|
|
40443
|
+
* Make the native `command_allowlist` authoritative for the `bash` allow rules
|
|
40444
|
+
* it can speak for, and for those only. The allowlist is a list of shell-command
|
|
40445
|
+
* patterns, so it is generated from `bash` alone (see `fromRulesyncPermissions`);
|
|
40446
|
+
* an allow in any other category names something Hermes's allowlist cannot
|
|
40447
|
+
* carry, so its absence from the list says nothing about it and it is kept as
|
|
40448
|
+
* provenance wrote it. The same holds for a `bash` allow the generator withheld
|
|
40449
|
+
* because a stricter `*` or `bash` rule covers it: the allowlist never carried
|
|
40450
|
+
* it, so its absence is not a retraction and the rule is kept too.
|
|
40451
|
+
*/
|
|
39718
40452
|
function reconcileCommandAllowlist({ permission, commandAllowlist }) {
|
|
39719
|
-
const
|
|
39720
|
-
const
|
|
39721
|
-
|
|
39722
|
-
|
|
39723
|
-
|
|
39724
|
-
|
|
39725
|
-
|
|
39726
|
-
for (const pattern of
|
|
39727
|
-
|
|
39728
|
-
|
|
39729
|
-
|
|
40453
|
+
const { rules: commandRules } = collectShellCommandRules(permission);
|
|
40454
|
+
const { shadowedAllowPatterns } = partitionCommandRules({
|
|
40455
|
+
rules: commandRules,
|
|
40456
|
+
writesAllToolsDeny: false
|
|
40457
|
+
});
|
|
40458
|
+
const withheld = new Set(shadowedAllowPatterns);
|
|
40459
|
+
const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
|
|
40460
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
|
|
40461
|
+
for (const pattern of commandAllowlist) rules[pattern] = "allow";
|
|
40462
|
+
}
|
|
40463
|
+
/**
|
|
40464
|
+
* Report the restricting rules Hermes has no per-pattern primitive for: a
|
|
40465
|
+
* `deny` or `ask` in any category other than `bash`, `*`, and `webfetch`, and
|
|
40466
|
+
* an `ask` under `webfetch` — the blocklist carries a `webfetch` deny but has
|
|
40467
|
+
* no ask tier. (`bash` and `*` are reported by `warnAboutUnwrittenCommandRules`.)
|
|
40468
|
+
* Such rules survive only in the round-trip blob.
|
|
40469
|
+
*/
|
|
40470
|
+
function warnAboutUnexpressedHermesRestrictions({ permissionBlock, foreignRestrictingCategories, logger }) {
|
|
40471
|
+
for (const category of foreignRestrictingCategories) {
|
|
40472
|
+
const isWebfetch = category === WEBFETCH_PERMISSION_CATEGORY;
|
|
40473
|
+
if (isWebfetch && patternsByAction(permissionBlock[category], "ask").length === 0) continue;
|
|
40474
|
+
warnWithFallback(logger, isWebfetch ? "Hermes Agent's security.website_blocklist has no ask tier, so the 'webfetch' ask rule(s) cannot be represented and were skipped; they survive only in the permissions.rulesync round-trip block." : `Hermes Agent has no per-pattern primitive for '${category}' deny and ask rules (it enforces command_allowlist, approvals.deny, and security.website_blocklist), so they were skipped; they survive only in the permissions.rulesync round-trip block.`);
|
|
39730
40475
|
}
|
|
39731
40476
|
}
|
|
39732
40477
|
function reconcileNativeDenies({ permission, category, patterns }) {
|
|
@@ -39838,14 +40583,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39838
40583
|
const approvals = isRecord$1(config.approvals) ? config.approvals : {};
|
|
39839
40584
|
reconcileNativeDenies({
|
|
39840
40585
|
permission,
|
|
39841
|
-
category:
|
|
40586
|
+
category: SHELL_PERMISSION_CATEGORY,
|
|
39842
40587
|
patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
|
|
39843
40588
|
});
|
|
39844
40589
|
const security = isRecord$1(config.security) ? config.security : {};
|
|
39845
40590
|
const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
|
|
39846
40591
|
reconcileNativeDenies({
|
|
39847
40592
|
permission,
|
|
39848
|
-
category:
|
|
40593
|
+
category: WEBFETCH_PERMISSION_CATEGORY,
|
|
39849
40594
|
patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
|
|
39850
40595
|
});
|
|
39851
40596
|
removeEmptyCategories(permission);
|
|
@@ -39865,12 +40610,32 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39865
40610
|
fileContent: JSON.stringify(imported, null, 2)
|
|
39866
40611
|
});
|
|
39867
40612
|
}
|
|
39868
|
-
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
|
|
40613
|
+
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false, logger }) {
|
|
39869
40614
|
const permissions = rulesyncPermissions.getJson();
|
|
39870
40615
|
const permissionBlock = permissions.permission ?? {};
|
|
39871
|
-
const
|
|
39872
|
-
const bashDeny =
|
|
39873
|
-
|
|
40616
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permissionBlock);
|
|
40617
|
+
const { allow: commandAllowlist, deny: bashDeny, shadowedAllowPatterns, unwrittenDenyPatterns, unenforcedAllToolsAskPatterns, intersectionBudgetExhausted } = partitionCommandRules({
|
|
40618
|
+
rules,
|
|
40619
|
+
writesAllToolsDeny: false
|
|
40620
|
+
});
|
|
40621
|
+
warnAboutUnexpressedHermesRestrictions({
|
|
40622
|
+
permissionBlock,
|
|
40623
|
+
foreignRestrictingCategories,
|
|
40624
|
+
logger
|
|
40625
|
+
});
|
|
40626
|
+
warnAboutUnwrittenCommandRules({
|
|
40627
|
+
toolLabel: "Hermes Agent",
|
|
40628
|
+
surfaceLabel: "command_allowlist/approvals.deny",
|
|
40629
|
+
foreignRestrictingCategories: [],
|
|
40630
|
+
shadowedAllowPatterns,
|
|
40631
|
+
unwrittenDenyPatterns,
|
|
40632
|
+
unwrittenDenyReason: "approvals.deny is a hard denylist of shell commands, and a pattern written under '*' need not be a command at all.",
|
|
40633
|
+
unenforcedAllToolsAskPatterns,
|
|
40634
|
+
ignoredAllToolsAllowPatterns,
|
|
40635
|
+
intersectionBudgetExhausted,
|
|
40636
|
+
logger
|
|
40637
|
+
});
|
|
40638
|
+
const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
|
|
39874
40639
|
let config = {};
|
|
39875
40640
|
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
39876
40641
|
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
@@ -40137,7 +40902,7 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
40137
40902
|
*/
|
|
40138
40903
|
function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
|
|
40139
40904
|
const ruleLists = {};
|
|
40140
|
-
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
40905
|
+
for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
40141
40906
|
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
40142
40907
|
if (!group) {
|
|
40143
40908
|
if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
|
|
@@ -40387,7 +41152,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
40387
41152
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
40388
41153
|
const kiloOverride = rulesyncJson.kilo;
|
|
40389
41154
|
const incomingPermission = {
|
|
40390
|
-
...rulesyncJson.permission,
|
|
41155
|
+
...honorAllToolsOnBash(rulesyncJson.permission),
|
|
40391
41156
|
...kiloOverride?.permission
|
|
40392
41157
|
};
|
|
40393
41158
|
const droppedDenyByKey = {};
|
|
@@ -40906,7 +41671,7 @@ function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
|
40906
41671
|
allowedCommands: [],
|
|
40907
41672
|
deniedCommands: []
|
|
40908
41673
|
};
|
|
40909
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
41674
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
40910
41675
|
if (action === "ask") {
|
|
40911
41676
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
40912
41677
|
continue;
|
|
@@ -41190,7 +41955,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
41190
41955
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
41191
41956
|
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
41192
41957
|
const sharedPermission = {};
|
|
41193
|
-
for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41958
|
+
for (const [category, value] of Object.entries(honorAllToolsOnBash(rulesyncJson.permission ?? {}))) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41194
41959
|
const permission = {};
|
|
41195
41960
|
for (const [category, value] of Object.entries({
|
|
41196
41961
|
...sharedPermission,
|
|
@@ -42319,11 +43084,19 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42319
43084
|
const paths = this.getSettablePaths();
|
|
42320
43085
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
42321
43086
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
42322
|
-
const
|
|
43087
|
+
const permission = rulesyncPermissions.getJson().permission;
|
|
43088
|
+
const bashStated = permission[COMMAND_CATEGORY] !== void 0;
|
|
42323
43089
|
const patch = {};
|
|
42324
|
-
if (
|
|
43090
|
+
if (bashStated) {
|
|
43091
|
+
const { bash } = resolveShellCommandLists({
|
|
43092
|
+
permission,
|
|
43093
|
+
writesAllToolsDeny: true,
|
|
43094
|
+
toolLabel: this.getToolLabel(),
|
|
43095
|
+
surfaceLabel: `${this.getAllowedCommandsKey()}/${this.getDeniedCommandsKey()}`,
|
|
43096
|
+
logger
|
|
43097
|
+
});
|
|
42325
43098
|
const { allowed, denied } = buildVscodeCommandLists({
|
|
42326
|
-
rules,
|
|
43099
|
+
rules: bash,
|
|
42327
43100
|
toolLabel: this.getToolLabel(),
|
|
42328
43101
|
logger
|
|
42329
43102
|
});
|
|
@@ -42334,7 +43107,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42334
43107
|
outputRoot,
|
|
42335
43108
|
relativeDirPath: paths.relativeDirPath,
|
|
42336
43109
|
relativeFilePath: paths.relativeFilePath,
|
|
42337
|
-
ownsCommandKeys:
|
|
43110
|
+
ownsCommandKeys: bashStated,
|
|
42338
43111
|
fileContent: applySharedConfigPatch({
|
|
42339
43112
|
fileKey: sharedConfigFileKey(paths),
|
|
42340
43113
|
feature: "permissions",
|
|
@@ -42714,7 +43487,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
42714
43487
|
config,
|
|
42715
43488
|
logger
|
|
42716
43489
|
});
|
|
42717
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
43490
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42718
43491
|
if (category === CATCH_ALL_PATTERN$1) {
|
|
42719
43492
|
const toolWideDefault = convertAllToolsRules({
|
|
42720
43493
|
rules,
|
|
@@ -43724,7 +44497,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
43724
44497
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43725
44498
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
43726
44499
|
const config = parseVibeConfig(existingContent);
|
|
43727
|
-
const permission = rulesyncPermissions.getJson().permission;
|
|
44500
|
+
const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
|
|
43728
44501
|
const vibeOverride = rulesyncPermissions.getJson().vibe;
|
|
43729
44502
|
const tools = toVibeToolsRecord(config.tools);
|
|
43730
44503
|
const diskShellPatterns = new Map([VIBE_SHELL_CATEGORY, ...VIBE_SHELL_ALIAS_TOOL_NAMES].map((vibeToolName) => [vibeToolName, toStringArray(readVibeToolConfig({
|
|
@@ -45065,6 +45838,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
45065
45838
|
isDeletable() {
|
|
45066
45839
|
return false;
|
|
45067
45840
|
}
|
|
45841
|
+
/**
|
|
45842
|
+
* `settings.toml` is Warp's file, not one rulesync owns: rulesync merges into
|
|
45843
|
+
* it when it exists but has no business bringing it into existence to hold
|
|
45844
|
+
* nothing. When no rule maps, both command lists are dropped and the payload
|
|
45845
|
+
* is a bare `[agents.profiles]` table, which would otherwise be written as a
|
|
45846
|
+
* fresh settings file that says nothing — an absent file and an empty table
|
|
45847
|
+
* both mean "Warp's own defaults". An existing file is still rewritten as
|
|
45848
|
+
* before, so user content is never dropped — the skip only applies when
|
|
45849
|
+
* there is no file yet.
|
|
45850
|
+
*/
|
|
45851
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
45852
|
+
return true;
|
|
45853
|
+
}
|
|
45068
45854
|
static getSettablePaths(_options) {
|
|
45069
45855
|
return {
|
|
45070
45856
|
relativeDirPath: warpSettingsDir(),
|
|
@@ -45676,6 +46462,7 @@ function buildZedToolPermissions({ permission, logger }) {
|
|
|
45676
46462
|
for (const [category, rules] of Object.entries(permission)) {
|
|
45677
46463
|
if (category === "*") {
|
|
45678
46464
|
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
46465
|
+
else if (permission.bash?.[pattern] === "deny" || permission.bash?.[pattern] === "ask") continue;
|
|
45679
46466
|
else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
|
|
45680
46467
|
continue;
|
|
45681
46468
|
}
|
|
@@ -45830,7 +46617,7 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
45830
46617
|
const toolPermissions = asRecord(agent.tool_permissions);
|
|
45831
46618
|
const existingTools = asRecord(toolPermissions.tools);
|
|
45832
46619
|
const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
|
|
45833
|
-
permission: config.permission,
|
|
46620
|
+
permission: honorAllToolsOnBash(config.permission),
|
|
45834
46621
|
logger
|
|
45835
46622
|
});
|
|
45836
46623
|
if (excludedCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${excludedCategories.map((category) => `"${category}"`).join(", ")} ${excludedCategories.length === 1 ? "category" : "categories"} — Zed does not gate its read-only tools, so the entries would never be consulted. Zed's read-denial surface is \`private_files\`, which the ignore feature writes from \`.rulesync/.aiignore\`.`);
|
|
@@ -46339,6 +47126,24 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
46339
47126
|
}
|
|
46340
47127
|
};
|
|
46341
47128
|
//#endregion
|
|
47129
|
+
//#region src/constants/codebuddy-paths.ts
|
|
47130
|
+
/**
|
|
47131
|
+
* CodeBuddy Code configuration-layout conventions.
|
|
47132
|
+
*
|
|
47133
|
+
* CodeBuddy Code (`@tencent-ai/codebuddy-code`) is Tencent Cloud's terminal
|
|
47134
|
+
* coding agent. Its configuration surface mirrors Claude Code closely: a
|
|
47135
|
+
* root memory file plus a `.codebuddy/` tree.
|
|
47136
|
+
*
|
|
47137
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
47138
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
47139
|
+
*/
|
|
47140
|
+
/** Root directory for CodeBuddy Code configuration, relative to the scope root. */
|
|
47141
|
+
const CODEBUDDY_DIR = ".codebuddy";
|
|
47142
|
+
const CODEBUDDY_RULE_FILE_NAME = "CODEBUDDY.md";
|
|
47143
|
+
const CODEBUDDY_LOCAL_RULE_FILE_NAME = "CODEBUDDY.local.md";
|
|
47144
|
+
/** Modular rules directory name under `.codebuddy/`. */
|
|
47145
|
+
const CODEBUDDY_RULES_DIR_NAME = "rules";
|
|
47146
|
+
//#endregion
|
|
46342
47147
|
//#region src/features/skills/simulated-skill.ts
|
|
46343
47148
|
const SimulatedSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
46344
47149
|
name: zod_mini.z.string(),
|
|
@@ -46877,7 +47682,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46877
47682
|
* This only deletes directories that are no longer in the rulesync source, not directories that will be overwritten.
|
|
46878
47683
|
*/
|
|
46879
47684
|
async removeOrphanAiDirs(existingDirs, generatedDirs) {
|
|
46880
|
-
const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
|
|
47685
|
+
const generatedPaths = new Set(generatedDirs.map((d) => caseFoldIdentity(d.getDirPath())));
|
|
46881
47686
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46882
47687
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46883
47688
|
for (const aiDir of existingDirs) {
|
|
@@ -46902,7 +47707,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46902
47707
|
this.logger.warn(verdict === "equal" ? `Refusing to delete ${quotedDirPath}: it is the root it was found in, not a directory inside that root` : `Refusing to delete ${quotedDirPath}: it is not inside ${quotedRoot}, the root it was found in`);
|
|
46903
47708
|
continue;
|
|
46904
47709
|
}
|
|
46905
|
-
if (!generatedPaths.has(dirPath)) orphanPaths.add(dirPath);
|
|
47710
|
+
if (!generatedPaths.has(caseFoldIdentity(dirPath))) orphanPaths.add(dirPath);
|
|
46906
47711
|
}
|
|
46907
47712
|
return await this.deleteOrphanPaths({
|
|
46908
47713
|
paths: orphanPaths,
|
|
@@ -47001,7 +47806,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47001
47806
|
const mainFile = aiDir.getMainFile();
|
|
47002
47807
|
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
47003
47808
|
for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
|
|
47004
|
-
const generatedNamesFolded = new Set([...generatedNames].map((name) => name
|
|
47809
|
+
const generatedNamesFolded = new Set([...generatedNames].map((name) => caseFoldIdentity(name)));
|
|
47005
47810
|
let existingNames;
|
|
47006
47811
|
try {
|
|
47007
47812
|
existingNames = await listFilePathsRecursively(dirPath, {
|
|
@@ -47016,7 +47821,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47016
47821
|
const posixName = toPosixPath(existingName);
|
|
47017
47822
|
if (generatedNames.has(posixName)) continue;
|
|
47018
47823
|
const filePath = (0, node_path.join)(dirPath, existingName);
|
|
47019
|
-
if (generatedNamesFolded.has(posixName
|
|
47824
|
+
if (generatedNamesFolded.has(caseFoldIdentity(posixName))) {
|
|
47020
47825
|
this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
47021
47826
|
continue;
|
|
47022
47827
|
}
|
|
@@ -47072,7 +47877,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47072
47877
|
generatedPaths.add(flatFilePath);
|
|
47073
47878
|
for (const file of generatedDir.getOtherFiles()) generatedPaths.add((0, node_path.join)(generatedDirPath, file.relativeFilePathToDirPath));
|
|
47074
47879
|
}
|
|
47075
|
-
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath
|
|
47880
|
+
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => caseFoldIdentity(generatedPath)));
|
|
47076
47881
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
47077
47882
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
47078
47883
|
for (const aiDir of existingFlatFiles) {
|
|
@@ -47102,7 +47907,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47102
47907
|
continue;
|
|
47103
47908
|
}
|
|
47104
47909
|
if (generatedPaths.has(filePath)) continue;
|
|
47105
|
-
if (generatedPathsFolded.has(filePath
|
|
47910
|
+
if (generatedPathsFolded.has(caseFoldIdentity(filePath))) {
|
|
47106
47911
|
this.logger.warn(`Refusing to delete ${quotedFilePath}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
47107
47912
|
continue;
|
|
47108
47913
|
}
|
|
@@ -48934,6 +49739,191 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
48934
49739
|
}
|
|
48935
49740
|
};
|
|
48936
49741
|
//#endregion
|
|
49742
|
+
//#region src/features/skills/crush-skill.ts
|
|
49743
|
+
const CrushSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
49744
|
+
name: zod_mini.z.string(),
|
|
49745
|
+
description: zod_mini.z.string(),
|
|
49746
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
49747
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
49748
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
49749
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
49750
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
49751
|
+
});
|
|
49752
|
+
/**
|
|
49753
|
+
* Represents a Crush Agent Skill directory.
|
|
49754
|
+
*
|
|
49755
|
+
* Crush auto-discovers Agent Skills (`SKILL.md` per directory) from
|
|
49756
|
+
* `.crush/skills/` at project scope and `~/.config/crush/skills/` (or
|
|
49757
|
+
* `$CRUSH_SKILLS_DIR`) at global scope. Unless `$CRUSH_SKILLS_DIR` is set,
|
|
49758
|
+
* Crush also scans several shared directories it does not own (globally
|
|
49759
|
+
* `~/.config/agents/skills/`, `~/.agents/skills/`, `~/.claude/skills/`;
|
|
49760
|
+
* per-project `.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, also
|
|
49761
|
+
* checked at a git worktree's common root); this class writes only to the
|
|
49762
|
+
* Crush-specific path above, leaving those shared roots to their own targets.
|
|
49763
|
+
*
|
|
49764
|
+
* Crush's `UserInvocable` field is a non-pointer Go `bool`, so an omitted
|
|
49765
|
+
* `user-invocable` (at both the root and the `crush:` section) resolves to
|
|
49766
|
+
* `false`: the skill stays reachable by the model but is hidden from Crush's
|
|
49767
|
+
* command palette. See `FromSkillCatalog` in `internal/commands/commands.go`.
|
|
49768
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
49769
|
+
*/
|
|
49770
|
+
var CrushSkill = class CrushSkill extends ToolSkill {
|
|
49771
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = CRUSH_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
49772
|
+
super({
|
|
49773
|
+
outputRoot,
|
|
49774
|
+
relativeDirPath,
|
|
49775
|
+
dirName,
|
|
49776
|
+
mainFile: {
|
|
49777
|
+
name: SKILL_FILE_NAME,
|
|
49778
|
+
body,
|
|
49779
|
+
frontmatter: { ...frontmatter }
|
|
49780
|
+
},
|
|
49781
|
+
otherFiles,
|
|
49782
|
+
global
|
|
49783
|
+
});
|
|
49784
|
+
if (validate) {
|
|
49785
|
+
const result = this.validate();
|
|
49786
|
+
if (!result.success) throw result.error;
|
|
49787
|
+
}
|
|
49788
|
+
}
|
|
49789
|
+
static getSettablePaths({ global = false } = {}) {
|
|
49790
|
+
return { relativeDirPath: global ? CRUSH_SKILLS_GLOBAL_DIR : CRUSH_SKILLS_PROJECT_DIR };
|
|
49791
|
+
}
|
|
49792
|
+
getFrontmatter() {
|
|
49793
|
+
return CrushSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
49794
|
+
}
|
|
49795
|
+
getBody() {
|
|
49796
|
+
return this.mainFile?.body ?? "";
|
|
49797
|
+
}
|
|
49798
|
+
validate() {
|
|
49799
|
+
if (!this.mainFile) return {
|
|
49800
|
+
success: false,
|
|
49801
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
49802
|
+
};
|
|
49803
|
+
const result = CrushSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
49804
|
+
if (!result.success) return {
|
|
49805
|
+
success: false,
|
|
49806
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
49807
|
+
};
|
|
49808
|
+
return {
|
|
49809
|
+
success: true,
|
|
49810
|
+
error: null
|
|
49811
|
+
};
|
|
49812
|
+
}
|
|
49813
|
+
toRulesyncSkill() {
|
|
49814
|
+
const frontmatter = this.getFrontmatter();
|
|
49815
|
+
const crushSection = {
|
|
49816
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
49817
|
+
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
49818
|
+
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
49819
|
+
...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
|
|
49820
|
+
...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
|
|
49821
|
+
};
|
|
49822
|
+
const rulesyncFrontmatter = {
|
|
49823
|
+
name: frontmatter.name,
|
|
49824
|
+
description: frontmatter.description,
|
|
49825
|
+
targets: ["*"],
|
|
49826
|
+
...Object.keys(crushSection).length > 0 && { crush: crushSection }
|
|
49827
|
+
};
|
|
49828
|
+
return new RulesyncSkill({
|
|
49829
|
+
outputRoot: this.outputRoot,
|
|
49830
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
49831
|
+
dirName: this.getDirName(),
|
|
49832
|
+
frontmatter: rulesyncFrontmatter,
|
|
49833
|
+
body: this.getBody(),
|
|
49834
|
+
otherFiles: this.getOtherFiles(),
|
|
49835
|
+
validate: true,
|
|
49836
|
+
global: this.global
|
|
49837
|
+
});
|
|
49838
|
+
}
|
|
49839
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49840
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49841
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49842
|
+
const crushSection = rulesyncFrontmatter.crush;
|
|
49843
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
49844
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49845
|
+
section: crushSection
|
|
49846
|
+
});
|
|
49847
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
49848
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49849
|
+
section: crushSection
|
|
49850
|
+
});
|
|
49851
|
+
const license = resolveLicense({
|
|
49852
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49853
|
+
section: crushSection
|
|
49854
|
+
});
|
|
49855
|
+
const compatibility = resolveCompatibility({
|
|
49856
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49857
|
+
section: crushSection
|
|
49858
|
+
});
|
|
49859
|
+
const metadata = resolveMetadata({
|
|
49860
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49861
|
+
section: crushSection
|
|
49862
|
+
});
|
|
49863
|
+
const compatibilityString = compatibility === void 0 ? void 0 : toCompatibilityString(compatibility);
|
|
49864
|
+
const crushFrontmatter = {
|
|
49865
|
+
name: rulesyncFrontmatter.name,
|
|
49866
|
+
description: rulesyncFrontmatter.description,
|
|
49867
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
49868
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
49869
|
+
...license !== void 0 && { license },
|
|
49870
|
+
...compatibilityString !== void 0 && compatibilityString.length > 0 && { compatibility: compatibilityString },
|
|
49871
|
+
...metadata !== void 0 && { metadata: toStringMetadata(metadata) }
|
|
49872
|
+
};
|
|
49873
|
+
return new CrushSkill({
|
|
49874
|
+
outputRoot,
|
|
49875
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
49876
|
+
dirName: rulesyncSkill.getDirName(),
|
|
49877
|
+
frontmatter: crushFrontmatter,
|
|
49878
|
+
body: rulesyncSkill.getBody(),
|
|
49879
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
49880
|
+
validate,
|
|
49881
|
+
global
|
|
49882
|
+
});
|
|
49883
|
+
}
|
|
49884
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
49885
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
49886
|
+
return targets.includes("*") || targets.includes("crush");
|
|
49887
|
+
}
|
|
49888
|
+
static async fromDir(params) {
|
|
49889
|
+
const loaded = await this.loadSkillDirContent({
|
|
49890
|
+
...params,
|
|
49891
|
+
getSettablePaths: CrushSkill.getSettablePaths
|
|
49892
|
+
});
|
|
49893
|
+
const result = CrushSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
49894
|
+
if (!result.success) {
|
|
49895
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
49896
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
49897
|
+
}
|
|
49898
|
+
return new CrushSkill({
|
|
49899
|
+
outputRoot: loaded.outputRoot,
|
|
49900
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
49901
|
+
dirName: loaded.dirName,
|
|
49902
|
+
frontmatter: result.data,
|
|
49903
|
+
body: loaded.body,
|
|
49904
|
+
otherFiles: loaded.otherFiles,
|
|
49905
|
+
validate: true,
|
|
49906
|
+
global: loaded.global
|
|
49907
|
+
});
|
|
49908
|
+
}
|
|
49909
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
49910
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49911
|
+
return new CrushSkill({
|
|
49912
|
+
outputRoot,
|
|
49913
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
49914
|
+
dirName,
|
|
49915
|
+
frontmatter: {
|
|
49916
|
+
name: "",
|
|
49917
|
+
description: ""
|
|
49918
|
+
},
|
|
49919
|
+
body: "",
|
|
49920
|
+
otherFiles: [],
|
|
49921
|
+
validate: false,
|
|
49922
|
+
global
|
|
49923
|
+
});
|
|
49924
|
+
}
|
|
49925
|
+
};
|
|
49926
|
+
//#endregion
|
|
48937
49927
|
//#region src/features/skills/cursor-skill.ts
|
|
48938
49928
|
const CursorSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
48939
49929
|
name: zod_mini.z.string(),
|
|
@@ -49098,9 +50088,9 @@ const DeepagentsSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
49098
50088
|
name: zod_mini.z.string(),
|
|
49099
50089
|
description: zod_mini.z.string(),
|
|
49100
50090
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
49101
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
49102
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
49103
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
50091
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50092
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50093
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown())
|
|
49104
50094
|
});
|
|
49105
50095
|
var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
|
|
49106
50096
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DEEPAGENTS_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -50158,9 +51148,9 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
50158
51148
|
const KiloSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
50159
51149
|
name: zod_mini.z.string(),
|
|
50160
51150
|
description: zod_mini.z.string(),
|
|
50161
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
50162
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
50163
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
51151
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51152
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51153
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50164
51154
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
50165
51155
|
});
|
|
50166
51156
|
var KiloSkill = class KiloSkill extends ToolSkill {
|
|
@@ -50597,10 +51587,11 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
50597
51587
|
rootFrontmatter: rulesyncFrontmatter,
|
|
50598
51588
|
section: kiroSection
|
|
50599
51589
|
});
|
|
51590
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = kiroSection ?? {};
|
|
50600
51591
|
const kiroFrontmatter = {
|
|
50601
|
-
...kiroSection,
|
|
50602
51592
|
name: rulesyncFrontmatter.name,
|
|
50603
51593
|
description: rulesyncFrontmatter.description,
|
|
51594
|
+
...section,
|
|
50604
51595
|
...license !== void 0 && { license },
|
|
50605
51596
|
...compatibility !== void 0 && { compatibility },
|
|
50606
51597
|
...metadata !== void 0 && { metadata }
|
|
@@ -50832,9 +51823,9 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
|
50832
51823
|
const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
50833
51824
|
name: zod_mini.z.string(),
|
|
50834
51825
|
description: zod_mini.z.string(),
|
|
50835
|
-
license: zod_mini.z.optional(zod_mini.z.
|
|
50836
|
-
compatibility: zod_mini.z.optional(zod_mini.z.
|
|
50837
|
-
metadata: zod_mini.z.optional(zod_mini.z.
|
|
51826
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51827
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
51828
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
50838
51829
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
50839
51830
|
});
|
|
50840
51831
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
@@ -52748,6 +53739,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
52748
53739
|
supportsGlobal: true
|
|
52749
53740
|
}
|
|
52750
53741
|
}],
|
|
53742
|
+
["crush", {
|
|
53743
|
+
class: CrushSkill,
|
|
53744
|
+
meta: {
|
|
53745
|
+
supportsProject: true,
|
|
53746
|
+
supportsSimulated: false,
|
|
53747
|
+
supportsGlobal: true
|
|
53748
|
+
}
|
|
53749
|
+
}],
|
|
52751
53750
|
["cursor", {
|
|
52752
53751
|
class: CursorSkill,
|
|
52753
53752
|
meta: {
|
|
@@ -60491,6 +61490,238 @@ var ClineRule = class ClineRule extends ToolRule {
|
|
|
60491
61490
|
}
|
|
60492
61491
|
};
|
|
60493
61492
|
//#endregion
|
|
61493
|
+
//#region src/features/rules/codebuddy-rule.ts
|
|
61494
|
+
/**
|
|
61495
|
+
* Frontmatter schema for CodeBuddy Code modular rules.
|
|
61496
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61497
|
+
*/
|
|
61498
|
+
const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
|
|
61499
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
61500
|
+
paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
61501
|
+
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean())
|
|
61502
|
+
});
|
|
61503
|
+
/**
|
|
61504
|
+
* A universal glob (matching everything) is redundant on an Always Apply
|
|
61505
|
+
* rule and, paired with `alwaysApply: true`, is the same semantic conflict
|
|
61506
|
+
* `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
|
|
61507
|
+
* applies the rule everywhere, so also emitting an explicit
|
|
61508
|
+
* `paths: ["**\/*"]` is at best redundant and, on a subsequent
|
|
61509
|
+
* import/generate round-trip, misleadingly implies the rule is scoped by
|
|
61510
|
+
* path rather than always-on.
|
|
61511
|
+
*/
|
|
61512
|
+
const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
61513
|
+
/**
|
|
61514
|
+
* Rule generator for CodeBuddy Code, Tencent Cloud's terminal coding agent
|
|
61515
|
+
* (`@tencent-ai/codebuddy-code`). Its configuration surface mirrors Claude
|
|
61516
|
+
* Code closely.
|
|
61517
|
+
*
|
|
61518
|
+
* Rules format:
|
|
61519
|
+
* - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
|
|
61520
|
+
* - {project}/.codebuddy/rules/*.md (root: false, with optional
|
|
61521
|
+
* `description` / `paths` / `alwaysApply` frontmatter)
|
|
61522
|
+
* - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
|
|
61523
|
+
*
|
|
61524
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61525
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
61526
|
+
*/
|
|
61527
|
+
var CodebuddyRule = class CodebuddyRule extends ToolRule {
|
|
61528
|
+
frontmatter;
|
|
61529
|
+
body;
|
|
61530
|
+
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61531
|
+
if (global) return {
|
|
61532
|
+
root: {
|
|
61533
|
+
relativeDirPath: buildToolPath(CODEBUDDY_DIR, ".", excludeToolDir),
|
|
61534
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61535
|
+
},
|
|
61536
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61537
|
+
};
|
|
61538
|
+
return {
|
|
61539
|
+
root: {
|
|
61540
|
+
relativeDirPath: ".",
|
|
61541
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61542
|
+
},
|
|
61543
|
+
alternativeRoots: [{
|
|
61544
|
+
relativeDirPath: CODEBUDDY_DIR,
|
|
61545
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61546
|
+
}],
|
|
61547
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61548
|
+
};
|
|
61549
|
+
}
|
|
61550
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
61551
|
+
if (rest.validate) {
|
|
61552
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61553
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
61554
|
+
}
|
|
61555
|
+
super({
|
|
61556
|
+
...rest,
|
|
61557
|
+
fileContent: rest.root ? body : CodebuddyRule.generateFileContent(body, frontmatter)
|
|
61558
|
+
});
|
|
61559
|
+
this.frontmatter = frontmatter;
|
|
61560
|
+
this.body = body;
|
|
61561
|
+
}
|
|
61562
|
+
static generateFileContent(body, frontmatter) {
|
|
61563
|
+
if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
|
|
61564
|
+
return stringifyFrontmatter(body, {
|
|
61565
|
+
description: frontmatter.description,
|
|
61566
|
+
alwaysApply: frontmatter.alwaysApply,
|
|
61567
|
+
paths: frontmatter.paths
|
|
61568
|
+
});
|
|
61569
|
+
}
|
|
61570
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, relativeDirPath: overrideDirPath }) {
|
|
61571
|
+
const paths = this.getSettablePaths({ global });
|
|
61572
|
+
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61573
|
+
const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
|
|
61574
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, rootDirPath, paths.root.relativeFilePath));
|
|
61575
|
+
return new CodebuddyRule({
|
|
61576
|
+
outputRoot,
|
|
61577
|
+
relativeDirPath: rootDirPath,
|
|
61578
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61579
|
+
frontmatter: {},
|
|
61580
|
+
body: fileContent.trim(),
|
|
61581
|
+
validate,
|
|
61582
|
+
root: true
|
|
61583
|
+
});
|
|
61584
|
+
}
|
|
61585
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
|
|
61586
|
+
const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
61587
|
+
const filePath = (0, node_path.join)(outputRoot, relativePath);
|
|
61588
|
+
const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
61589
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61590
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
61591
|
+
return new CodebuddyRule({
|
|
61592
|
+
outputRoot,
|
|
61593
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61594
|
+
relativeFilePath,
|
|
61595
|
+
frontmatter: result.data,
|
|
61596
|
+
body: content.trim(),
|
|
61597
|
+
validate,
|
|
61598
|
+
root: false
|
|
61599
|
+
});
|
|
61600
|
+
}
|
|
61601
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61602
|
+
const isRoot = relativeFilePath === this.getSettablePaths({ global }).root.relativeFilePath;
|
|
61603
|
+
return new CodebuddyRule({
|
|
61604
|
+
outputRoot,
|
|
61605
|
+
relativeDirPath,
|
|
61606
|
+
relativeFilePath,
|
|
61607
|
+
frontmatter: {},
|
|
61608
|
+
body: "",
|
|
61609
|
+
validate: false,
|
|
61610
|
+
root: isRoot
|
|
61611
|
+
});
|
|
61612
|
+
}
|
|
61613
|
+
static resolveCodebuddyPaths({ paths, alwaysApply }) {
|
|
61614
|
+
if (!paths || paths.length === 0) return;
|
|
61615
|
+
if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
|
|
61616
|
+
return paths;
|
|
61617
|
+
}
|
|
61618
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
61619
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
61620
|
+
const root = rulesyncFrontmatter.root ?? false;
|
|
61621
|
+
const paths = this.getSettablePaths({ global });
|
|
61622
|
+
const body = rulesyncRule.getBody();
|
|
61623
|
+
if (root) return new CodebuddyRule({
|
|
61624
|
+
outputRoot,
|
|
61625
|
+
frontmatter: {},
|
|
61626
|
+
body,
|
|
61627
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
61628
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61629
|
+
validate,
|
|
61630
|
+
root
|
|
61631
|
+
});
|
|
61632
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
|
|
61633
|
+
const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
|
|
61634
|
+
const globs = rulesyncFrontmatter.globs;
|
|
61635
|
+
const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
|
|
61636
|
+
const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
|
|
61637
|
+
paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
|
|
61638
|
+
alwaysApply: alwaysApply === true
|
|
61639
|
+
});
|
|
61640
|
+
const codebuddyFrontmatter = {
|
|
61641
|
+
description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
|
|
61642
|
+
paths: pathsValue,
|
|
61643
|
+
alwaysApply
|
|
61644
|
+
};
|
|
61645
|
+
return new CodebuddyRule({
|
|
61646
|
+
outputRoot,
|
|
61647
|
+
frontmatter: codebuddyFrontmatter,
|
|
61648
|
+
body,
|
|
61649
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61650
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
61651
|
+
validate,
|
|
61652
|
+
root
|
|
61653
|
+
});
|
|
61654
|
+
}
|
|
61655
|
+
toRulesyncRule() {
|
|
61656
|
+
const targets = ["*"];
|
|
61657
|
+
if (this.isRoot()) {
|
|
61658
|
+
const rulesyncFrontmatter = {
|
|
61659
|
+
targets,
|
|
61660
|
+
root: true,
|
|
61661
|
+
description: this.description,
|
|
61662
|
+
globs: ["**/*"]
|
|
61663
|
+
};
|
|
61664
|
+
return new RulesyncRule({
|
|
61665
|
+
outputRoot: this.getOutputRoot(),
|
|
61666
|
+
frontmatter: rulesyncFrontmatter,
|
|
61667
|
+
body: this.body,
|
|
61668
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61669
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61670
|
+
validate: true
|
|
61671
|
+
});
|
|
61672
|
+
}
|
|
61673
|
+
const isAlways = this.frontmatter.alwaysApply === true;
|
|
61674
|
+
const sourcePaths = this.frontmatter.paths ?? [];
|
|
61675
|
+
const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
|
|
61676
|
+
const rulesyncFrontmatter = {
|
|
61677
|
+
targets,
|
|
61678
|
+
root: false,
|
|
61679
|
+
description: this.frontmatter.description,
|
|
61680
|
+
globs,
|
|
61681
|
+
...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
|
|
61682
|
+
paths: this.frontmatter.paths,
|
|
61683
|
+
alwaysApply: this.frontmatter.alwaysApply,
|
|
61684
|
+
description: this.frontmatter.description
|
|
61685
|
+
} }
|
|
61686
|
+
};
|
|
61687
|
+
return new RulesyncRule({
|
|
61688
|
+
outputRoot: this.getOutputRoot(),
|
|
61689
|
+
frontmatter: rulesyncFrontmatter,
|
|
61690
|
+
body: this.body,
|
|
61691
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61692
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61693
|
+
validate: true
|
|
61694
|
+
});
|
|
61695
|
+
}
|
|
61696
|
+
validate() {
|
|
61697
|
+
if (!this.frontmatter) return {
|
|
61698
|
+
success: true,
|
|
61699
|
+
error: null
|
|
61700
|
+
};
|
|
61701
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
61702
|
+
if (result.success) return {
|
|
61703
|
+
success: true,
|
|
61704
|
+
error: null
|
|
61705
|
+
};
|
|
61706
|
+
else return {
|
|
61707
|
+
success: false,
|
|
61708
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
61709
|
+
};
|
|
61710
|
+
}
|
|
61711
|
+
getFrontmatter() {
|
|
61712
|
+
return this.frontmatter;
|
|
61713
|
+
}
|
|
61714
|
+
getBody() {
|
|
61715
|
+
return this.body;
|
|
61716
|
+
}
|
|
61717
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
61718
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
61719
|
+
rulesyncRule,
|
|
61720
|
+
toolTarget: "codebuddy"
|
|
61721
|
+
});
|
|
61722
|
+
}
|
|
61723
|
+
};
|
|
61724
|
+
//#endregion
|
|
60494
61725
|
//#region src/features/rules/codexcli-rule.ts
|
|
60495
61726
|
var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
60496
61727
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -60787,6 +62018,79 @@ var CopilotcliRule = class CopilotcliRule extends CopilotRule {
|
|
|
60787
62018
|
}
|
|
60788
62019
|
};
|
|
60789
62020
|
//#endregion
|
|
62021
|
+
//#region src/features/rules/crush-rule.ts
|
|
62022
|
+
var CrushRule = class CrushRule extends ToolRule {
|
|
62023
|
+
constructor({ fileContent, root, ...rest }) {
|
|
62024
|
+
super({
|
|
62025
|
+
...rest,
|
|
62026
|
+
fileContent,
|
|
62027
|
+
root: root ?? false
|
|
62028
|
+
});
|
|
62029
|
+
}
|
|
62030
|
+
static getSettablePaths({ global = false } = {}) {
|
|
62031
|
+
if (global) return { root: {
|
|
62032
|
+
relativeDirPath: CRUSH_GLOBAL_DIR,
|
|
62033
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
62034
|
+
} };
|
|
62035
|
+
return { root: {
|
|
62036
|
+
relativeDirPath: ".",
|
|
62037
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
62038
|
+
} };
|
|
62039
|
+
}
|
|
62040
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
62041
|
+
const { root } = this.getSettablePaths({ global });
|
|
62042
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
62043
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
62044
|
+
return new CrushRule({
|
|
62045
|
+
outputRoot,
|
|
62046
|
+
relativeDirPath: root.relativeDirPath,
|
|
62047
|
+
relativeFilePath: root.relativeFilePath,
|
|
62048
|
+
fileContent,
|
|
62049
|
+
validate,
|
|
62050
|
+
root: true
|
|
62051
|
+
});
|
|
62052
|
+
}
|
|
62053
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
62054
|
+
const { root } = this.getSettablePaths({ global });
|
|
62055
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
62056
|
+
return new CrushRule({
|
|
62057
|
+
outputRoot,
|
|
62058
|
+
relativeDirPath: root.relativeDirPath,
|
|
62059
|
+
relativeFilePath: root.relativeFilePath,
|
|
62060
|
+
fileContent: rulesyncRule.getBody(),
|
|
62061
|
+
validate,
|
|
62062
|
+
root: isRoot
|
|
62063
|
+
});
|
|
62064
|
+
}
|
|
62065
|
+
toRulesyncRule() {
|
|
62066
|
+
return this.toRulesyncRuleDefault();
|
|
62067
|
+
}
|
|
62068
|
+
validate() {
|
|
62069
|
+
return {
|
|
62070
|
+
success: true,
|
|
62071
|
+
error: null
|
|
62072
|
+
};
|
|
62073
|
+
}
|
|
62074
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
62075
|
+
const { root } = this.getSettablePaths({ global });
|
|
62076
|
+
const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
|
|
62077
|
+
return new CrushRule({
|
|
62078
|
+
outputRoot,
|
|
62079
|
+
relativeDirPath,
|
|
62080
|
+
relativeFilePath,
|
|
62081
|
+
fileContent: "",
|
|
62082
|
+
validate: false,
|
|
62083
|
+
root: isRoot
|
|
62084
|
+
});
|
|
62085
|
+
}
|
|
62086
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
62087
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
62088
|
+
rulesyncRule,
|
|
62089
|
+
toolTarget: "crush"
|
|
62090
|
+
});
|
|
62091
|
+
}
|
|
62092
|
+
};
|
|
62093
|
+
//#endregion
|
|
60790
62094
|
//#region src/features/rules/cursor-rule.ts
|
|
60791
62095
|
const CursorRuleFrontmatterSchema = zod_mini.z.object({
|
|
60792
62096
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -61394,13 +62698,34 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
61394
62698
|
};
|
|
61395
62699
|
//#endregion
|
|
61396
62700
|
//#region src/features/rules/factorydroid-rule.ts
|
|
62701
|
+
/**
|
|
62702
|
+
* Rule generator for Factory Droid.
|
|
62703
|
+
*
|
|
62704
|
+
* Factory Droid loads the root `AGENTS.md` (project) / `~/.factory/AGENTS.md`
|
|
62705
|
+
* (global) as coding guidelines, plus non-root rules referenced from it via
|
|
62706
|
+
* `.factory/rules/*.md`.
|
|
62707
|
+
*
|
|
62708
|
+
* Factory Droid also loads `DESIGN.md` (project only) as a second,
|
|
62709
|
+
* independent instruction surface: "Always-on design-system, UX, visual, and
|
|
62710
|
+
* interaction guidance", loaded separately from `AGENTS.md`'s coding
|
|
62711
|
+
* guidelines. Rulesync emits it from any non-root rule that opts in via a
|
|
62712
|
+
* `factorydroid.channel: design` frontmatter block — those rule bodies are
|
|
62713
|
+
* routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
|
|
62714
|
+
* multiple opted-in rules concatenate in source order. Factory's docs describe
|
|
62715
|
+
* `DESIGN.md` at the repository root and in nested subdirectories, like
|
|
62716
|
+
* `AGENTS.md`, but document no personal/global home-directory equivalent, so
|
|
62717
|
+
* this channel is project scope only.
|
|
62718
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
62719
|
+
*/
|
|
61397
62720
|
var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
61398
|
-
|
|
62721
|
+
design;
|
|
62722
|
+
constructor({ fileContent, root, design = false, ...rest }) {
|
|
61399
62723
|
super({
|
|
61400
62724
|
...rest,
|
|
61401
62725
|
fileContent,
|
|
61402
62726
|
root: root ?? false
|
|
61403
62727
|
});
|
|
62728
|
+
this.design = design;
|
|
61404
62729
|
}
|
|
61405
62730
|
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61406
62731
|
if (global) return { root: {
|
|
@@ -61412,11 +62737,47 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61412
62737
|
relativeDirPath: ".",
|
|
61413
62738
|
relativeFilePath: FACTORYDROID_RULE_FILE_NAME
|
|
61414
62739
|
},
|
|
61415
|
-
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) }
|
|
62740
|
+
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) },
|
|
62741
|
+
design: {
|
|
62742
|
+
relativeDirPath: ".",
|
|
62743
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
|
|
62744
|
+
}
|
|
61416
62745
|
};
|
|
61417
62746
|
}
|
|
61418
|
-
|
|
62747
|
+
/**
|
|
62748
|
+
* Extra fixed files this tool manages beyond the root/non-root rules. The
|
|
62749
|
+
* RulesProcessor enumerates these for import and deletion so a stale
|
|
62750
|
+
* `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
|
|
62751
|
+
* mode: `DESIGN.md` has no documented home-directory equivalent.
|
|
62752
|
+
*/
|
|
62753
|
+
static getExtraFixedFiles({ global = false } = {}) {
|
|
62754
|
+
if (global) return [];
|
|
62755
|
+
return [this.getSettablePaths({ global }).design];
|
|
62756
|
+
}
|
|
62757
|
+
/**
|
|
62758
|
+
* Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
|
|
62759
|
+
* TOON reference section would double-load the content (and misrepresent it
|
|
62760
|
+
* as a rule the model must remember to open).
|
|
62761
|
+
*/
|
|
62762
|
+
isExcludedFromRootReferences() {
|
|
62763
|
+
return this.design;
|
|
62764
|
+
}
|
|
62765
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
61419
62766
|
const paths = this.getSettablePaths({ global });
|
|
62767
|
+
const design = !global ? paths.design : void 0;
|
|
62768
|
+
if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
|
|
62769
|
+
const relativePath = (0, node_path.join)(design.relativeDirPath, design.relativeFilePath);
|
|
62770
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
62771
|
+
return new FactorydroidRule({
|
|
62772
|
+
outputRoot,
|
|
62773
|
+
relativeDirPath: design.relativeDirPath,
|
|
62774
|
+
relativeFilePath: design.relativeFilePath,
|
|
62775
|
+
fileContent,
|
|
62776
|
+
validate,
|
|
62777
|
+
root: false,
|
|
62778
|
+
design: true
|
|
62779
|
+
});
|
|
62780
|
+
}
|
|
61420
62781
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61421
62782
|
const relativePath = (0, node_path.join)(paths.root.relativeDirPath, paths.root.relativeFilePath);
|
|
61422
62783
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
@@ -61443,18 +62804,34 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61443
62804
|
}
|
|
61444
62805
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61445
62806
|
const paths = this.getSettablePaths({ global });
|
|
61446
|
-
const
|
|
62807
|
+
const design = !global ? paths.design : void 0;
|
|
62808
|
+
const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
|
|
62809
|
+
const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
61447
62810
|
return new FactorydroidRule({
|
|
61448
62811
|
outputRoot,
|
|
61449
62812
|
relativeDirPath,
|
|
61450
62813
|
relativeFilePath,
|
|
61451
62814
|
fileContent: "",
|
|
61452
62815
|
validate: false,
|
|
61453
|
-
root: isRoot
|
|
62816
|
+
root: isRoot,
|
|
62817
|
+
design: isDesign
|
|
61454
62818
|
});
|
|
61455
62819
|
}
|
|
61456
62820
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
62821
|
+
const frontmatter = rulesyncRule.getFrontmatter();
|
|
61457
62822
|
const paths = this.getSettablePaths({ global });
|
|
62823
|
+
if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
|
|
62824
|
+
const { design } = paths;
|
|
62825
|
+
return new FactorydroidRule({
|
|
62826
|
+
outputRoot,
|
|
62827
|
+
relativeDirPath: design.relativeDirPath,
|
|
62828
|
+
relativeFilePath: design.relativeFilePath,
|
|
62829
|
+
fileContent: rulesyncRule.getBody(),
|
|
62830
|
+
validate,
|
|
62831
|
+
root: false,
|
|
62832
|
+
design: true
|
|
62833
|
+
});
|
|
62834
|
+
}
|
|
61458
62835
|
return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
|
|
61459
62836
|
outputRoot,
|
|
61460
62837
|
rulesyncRule,
|
|
@@ -61464,6 +62841,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61464
62841
|
}));
|
|
61465
62842
|
}
|
|
61466
62843
|
toRulesyncRule() {
|
|
62844
|
+
if (this.design) return new RulesyncRule({
|
|
62845
|
+
outputRoot: process.cwd(),
|
|
62846
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
62847
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
|
|
62848
|
+
frontmatter: {
|
|
62849
|
+
root: false,
|
|
62850
|
+
targets: ["factorydroid"],
|
|
62851
|
+
factorydroid: { channel: "design" }
|
|
62852
|
+
},
|
|
62853
|
+
body: this.getFileContent()
|
|
62854
|
+
});
|
|
61467
62855
|
return this.toRulesyncRuleDefault();
|
|
61468
62856
|
}
|
|
61469
62857
|
validate() {
|
|
@@ -63981,6 +65369,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
63981
65369
|
ruleDiscoveryMode: "auto"
|
|
63982
65370
|
}
|
|
63983
65371
|
}],
|
|
65372
|
+
["codebuddy", {
|
|
65373
|
+
class: CodebuddyRule,
|
|
65374
|
+
meta: {
|
|
65375
|
+
extension: "md",
|
|
65376
|
+
supportsGlobal: true,
|
|
65377
|
+
ruleDiscoveryMode: "auto",
|
|
65378
|
+
localRootMode: "separate-local-file",
|
|
65379
|
+
localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
|
|
65380
|
+
}
|
|
65381
|
+
}],
|
|
63984
65382
|
["codexcli", {
|
|
63985
65383
|
class: CodexcliRule,
|
|
63986
65384
|
meta: {
|
|
@@ -64006,6 +65404,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
64006
65404
|
ruleDiscoveryMode: "auto"
|
|
64007
65405
|
}
|
|
64008
65406
|
}],
|
|
65407
|
+
["crush", {
|
|
65408
|
+
class: CrushRule,
|
|
65409
|
+
meta: {
|
|
65410
|
+
extension: "md",
|
|
65411
|
+
supportsGlobal: true,
|
|
65412
|
+
ruleDiscoveryMode: "auto",
|
|
65413
|
+
collisionPolicy: "fold"
|
|
65414
|
+
}
|
|
65415
|
+
}],
|
|
64009
65416
|
["cursor", {
|
|
64010
65417
|
class: CursorRule,
|
|
64011
65418
|
meta: {
|
|
@@ -64374,6 +65781,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64374
65781
|
outputFiles,
|
|
64375
65782
|
convertedRules
|
|
64376
65783
|
});
|
|
65784
|
+
await this.warnForDeactivatedImportOnlyRoots({
|
|
65785
|
+
toolRules,
|
|
65786
|
+
factory
|
|
65787
|
+
});
|
|
64377
65788
|
return outputFiles;
|
|
64378
65789
|
}
|
|
64379
65790
|
/**
|
|
@@ -64595,6 +66006,41 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64595
66006
|
}
|
|
64596
66007
|
}
|
|
64597
66008
|
/**
|
|
66009
|
+
* Warn when this generate run is about to write a root rule file that will
|
|
66010
|
+
* make the tool stop reading paths it currently reads instead — Junie's
|
|
66011
|
+
* `.junie/rules/*.md` and `.junie/playbook.md` become unreachable the
|
|
66012
|
+
* moment `.junie/AGENTS.md` exists, since Junie reads the root file
|
|
66013
|
+
* exclusively once it is present. `importOnlyRoots` with
|
|
66014
|
+
* `onlyWhenRootAbsent` already models exactly this shape for import; this
|
|
66015
|
+
* reuses the same declaration so the
|
|
66016
|
+
* `generate` path — which never calls `loadToolFiles` and so never reached
|
|
66017
|
+
* the existing import-side warning — surfaces it too. Without this, a repo
|
|
66018
|
+
* that only ever runs `generate` never sees any warning: the deactivated
|
|
66019
|
+
* files stay on disk, untouched and not gitignored, silently unread.
|
|
66020
|
+
*/
|
|
66021
|
+
async warnForDeactivatedImportOnlyRoots({ toolRules, factory }) {
|
|
66022
|
+
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
66023
|
+
if (!rootRule) return;
|
|
66024
|
+
const settablePaths = factory.class.getSettablePaths({ global: this.global });
|
|
66025
|
+
const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
|
|
66026
|
+
if (!importOnlyRoots || importOnlyRoots.length === 0) return;
|
|
66027
|
+
const existingPaths = [];
|
|
66028
|
+
for (const importOnlyRoot of importOnlyRoots) {
|
|
66029
|
+
if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
|
|
66030
|
+
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
66031
|
+
cwd: this.outputRoot,
|
|
66032
|
+
type: "file"
|
|
66033
|
+
});
|
|
66034
|
+
existingPaths.push(...matchedPaths);
|
|
66035
|
+
}
|
|
66036
|
+
if (existingPaths.length === 0) return;
|
|
66037
|
+
const rootFileRelativePath = (0, node_path.join)(rootRule.getRelativeDirPath(), rootRule.getRelativeFilePath());
|
|
66038
|
+
const names = existingPaths.map((filePath) => stripControlCharacters((0, node_path.relative)(this.outputRoot, filePath)));
|
|
66039
|
+
const listedNames = names.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
66040
|
+
const remainingCount = names.length - listedNames.length;
|
|
66041
|
+
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.`);
|
|
66042
|
+
}
|
|
66043
|
+
/**
|
|
64598
66044
|
* Handle localRoot rule generation based on tool target.
|
|
64599
66045
|
* - `separate-local-file`: writes a dedicated `*.local.md` root file
|
|
64600
66046
|
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
|
|
@@ -64846,6 +66292,26 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64846
66292
|
}));
|
|
64847
66293
|
}
|
|
64848
66294
|
/**
|
|
66295
|
+
* Load and merge rulesync rule files from every configured input root's
|
|
66296
|
+
* `.rulesync/rules/` directory, by relative path, so that a rule with the
|
|
66297
|
+
* same target path from a later root replaces the earlier root's copy
|
|
66298
|
+
* (case-insensitive, matching the intra-root collision handling).
|
|
66299
|
+
*
|
|
66300
|
+
* This is the side-effect-free half of `loadRulesyncFiles`: it does not
|
|
66301
|
+
* warn about a missing root rule or validate `localRoot` placement, so it
|
|
66302
|
+
* is also safe to call from code paths — like
|
|
66303
|
+
* `warnForFoldImportDuplicationRisk` — that only need the merged rule set
|
|
66304
|
+
* and must not trigger `loadRulesyncFiles`'s generate-time checks.
|
|
66305
|
+
*/
|
|
66306
|
+
async loadMergedRulesyncRules() {
|
|
66307
|
+
return mergeByCaseInsensitiveIdentity({
|
|
66308
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
66309
|
+
identity: (rule) => rule.getRelativeFilePath(),
|
|
66310
|
+
artifactName: "rule",
|
|
66311
|
+
logger: this.logger
|
|
66312
|
+
});
|
|
66313
|
+
}
|
|
66314
|
+
/**
|
|
64849
66315
|
* Implementation of abstract method from FeatureProcessor
|
|
64850
66316
|
* Load and parse rulesync rule files from every configured input root's
|
|
64851
66317
|
* `.rulesync/rules/` directory, merging by relative path so that a rule
|
|
@@ -64853,12 +66319,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64853
66319
|
* copy (case-insensitive, matching the intra-root collision handling).
|
|
64854
66320
|
*/
|
|
64855
66321
|
async loadRulesyncFiles() {
|
|
64856
|
-
const rulesyncRules =
|
|
64857
|
-
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
64858
|
-
identity: (rule) => rule.getRelativeFilePath(),
|
|
64859
|
-
artifactName: "rule",
|
|
64860
|
-
logger: this.logger
|
|
64861
|
-
});
|
|
66322
|
+
const rulesyncRules = await this.loadMergedRulesyncRules();
|
|
64862
66323
|
const factory = this.getFactory(this.toolTarget);
|
|
64863
66324
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
64864
66325
|
if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
|
|
@@ -64915,6 +66376,46 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64915
66376
|
});
|
|
64916
66377
|
}
|
|
64917
66378
|
/**
|
|
66379
|
+
* Warn when importing a `collisionPolicy: "fold"` target's root file while
|
|
66380
|
+
* `.rulesync/rules/` still holds non-root rules targeting it. A fold target
|
|
66381
|
+
* (codexcli, junie, and others) concatenates every targeted non-root rule
|
|
66382
|
+
* into its one root output file on `generate`. Importing that root file
|
|
66383
|
+
* back therefore re-reads the already-folded content as a single new
|
|
66384
|
+
* rulesync rule, while the original non-root rules stay in place
|
|
66385
|
+
* untouched — the next `generate` folds both together, duplicating the
|
|
66386
|
+
* content once per generate/import cycle with nothing to indicate why.
|
|
66387
|
+
*
|
|
66388
|
+
* This does not attempt to detect or drop the specific duplicated content
|
|
66389
|
+
* (the root file has no marker recording which rule contributed what); it
|
|
66390
|
+
* only surfaces that the cycle produces one, per the "at minimum, warn"
|
|
66391
|
+
* option recorded on issue #2743.
|
|
66392
|
+
*
|
|
66393
|
+
* Only the actual `rulesync import` call site invokes this (and only once
|
|
66394
|
+
* it has confirmed there is something to import) — `loadToolFiles` is also
|
|
66395
|
+
* the entry point for `rulesync convert` and `rulesync fetch`, neither of
|
|
66396
|
+
* which writes to `.rulesync/rules/` or carries this duplication risk.
|
|
66397
|
+
*
|
|
66398
|
+
* Reads via `loadMergedRulesyncRules` rather than `loadRulesyncFiles`
|
|
66399
|
+
* deliberately: this runs before the imported root file is written, so
|
|
66400
|
+
* `.rulesync/rules/` never yet has a root rule targeting this tool, and
|
|
66401
|
+
* `loadRulesyncFiles`'s "no root rule found" warning and `localRoot`
|
|
66402
|
+
* validation (which can throw) would fire spuriously on every fold-tool
|
|
66403
|
+
* import — including ones where nothing is actually misconfigured.
|
|
66404
|
+
*
|
|
66405
|
+
* In global mode, a `localRoot: true` rule is excluded from the
|
|
66406
|
+
* duplication check the same way `loadRulesyncFiles`'s global-mode branch
|
|
66407
|
+
* excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
|
|
66408
|
+
* in global mode, so such a rule is never actually folded into the global
|
|
66409
|
+
* root output and warning about it here would be inaccurate.
|
|
66410
|
+
*/
|
|
66411
|
+
async warnForFoldImportDuplicationRisk() {
|
|
66412
|
+
const factory = this.getFactory(this.toolTarget);
|
|
66413
|
+
if (factory.meta.collisionPolicy !== "fold") return;
|
|
66414
|
+
const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
|
|
66415
|
+
if (nonRootRules.length === 0) return;
|
|
66416
|
+
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.`);
|
|
66417
|
+
}
|
|
66418
|
+
/**
|
|
64918
66419
|
* Implementation of abstract method from FeatureProcessor
|
|
64919
66420
|
* Load tool-specific rule configurations and parse them into ToolRule instances
|
|
64920
66421
|
*/
|
|
@@ -66978,6 +68479,7 @@ async function importRulesCore(params) {
|
|
|
66978
68479
|
logger.warn(`No rule files found for ${tool}. Skipping import.`);
|
|
66979
68480
|
return 0;
|
|
66980
68481
|
}
|
|
68482
|
+
await rulesProcessor.warnForFoldImportDuplicationRisk();
|
|
66981
68483
|
const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
66982
68484
|
const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
|
|
66983
68485
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);
|
|
@@ -67297,6 +68799,18 @@ Object.defineProperty(exports, "CLIError", {
|
|
|
67297
68799
|
return CLIError;
|
|
67298
68800
|
}
|
|
67299
68801
|
});
|
|
68802
|
+
Object.defineProperty(exports, "CODEBUDDY_DIR", {
|
|
68803
|
+
enumerable: true,
|
|
68804
|
+
get: function() {
|
|
68805
|
+
return CODEBUDDY_DIR;
|
|
68806
|
+
}
|
|
68807
|
+
});
|
|
68808
|
+
Object.defineProperty(exports, "CODEBUDDY_LOCAL_RULE_FILE_NAME", {
|
|
68809
|
+
enumerable: true,
|
|
68810
|
+
get: function() {
|
|
68811
|
+
return CODEBUDDY_LOCAL_RULE_FILE_NAME;
|
|
68812
|
+
}
|
|
68813
|
+
});
|
|
67300
68814
|
Object.defineProperty(exports, "CODEXCLI_BASH_RULES_FILE_NAME", {
|
|
67301
68815
|
enumerable: true,
|
|
67302
68816
|
get: function() {
|
|
@@ -67879,6 +69393,12 @@ Object.defineProperty(exports, "hasDeceptiveHiddenCharacters", {
|
|
|
67879
69393
|
return hasDeceptiveHiddenCharacters;
|
|
67880
69394
|
}
|
|
67881
69395
|
});
|
|
69396
|
+
Object.defineProperty(exports, "hasEnclosingMarkOutsideKeycap", {
|
|
69397
|
+
enumerable: true,
|
|
69398
|
+
get: function() {
|
|
69399
|
+
return hasEnclosingMarkOutsideKeycap;
|
|
69400
|
+
}
|
|
69401
|
+
});
|
|
67882
69402
|
Object.defineProperty(exports, "importFromTool", {
|
|
67883
69403
|
enumerable: true,
|
|
67884
69404
|
get: function() {
|