rulesync 43.6.1 → 43.6.2

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.
@@ -2269,6 +2269,190 @@ async function removeTempDirectory(tempDir) {
2269
2269
  });
2270
2270
  } catch {}
2271
2271
  }
2272
+ // src/utils/prototype-pollution.ts
2273
+ /**
2274
+ * Keys that, if walked into when constructing or merging objects from
2275
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
2276
+ * chain) and propagate state to every other object in the runtime. Any code
2277
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
2278
+ * parsing, MCP config conversion, settings round-trip — should skip these.
2279
+ */
2280
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
2281
+ "__proto__",
2282
+ "constructor",
2283
+ "prototype"
2284
+ ]);
2285
+ function isPrototypePollutionKey(key) {
2286
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
2287
+ }
2288
+ /**
2289
+ * Returns a shallow copy of a record's own entries with every
2290
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
2291
+ *
2292
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
2293
+ * or `headers` table — into freshly generated config. Carrying such a map by
2294
+ * reference, or re-assigning its keys via bracket notation, would let a literal
2295
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
2296
+ * prototype). Walking the entries through this helper severs that path while
2297
+ * preserving every legitimate key.
2298
+ */
2299
+ function omitPrototypePollutionKeys(record) {
2300
+ const sanitized = {};
2301
+ for (const [key, value] of Object.entries(record)) {
2302
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2303
+ sanitized[key] = value;
2304
+ }
2305
+ return sanitized;
2306
+ }
2307
+ /**
2308
+ * Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
2309
+ * reachable through the value (directly, or through an array) is copied with
2310
+ * its prototype-pollution keys dropped. Scalars and arrays of scalars come back
2311
+ * as they are.
2312
+ *
2313
+ * Use when passing an unknown, user-supplied value through unchanged — an MCP
2314
+ * server field a tool documents that rulesync does not model — so a
2315
+ * `__proto__` key nested anywhere inside it cannot ride into the generated
2316
+ * config, where the consuming tool may merge the object without the same care.
2317
+ */
2318
+ function omitPrototypePollutionKeysDeep(value) {
2319
+ if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
2320
+ if (value === null || typeof value !== "object") return value;
2321
+ const sanitized = {};
2322
+ for (const [key, entry] of Object.entries(value)) {
2323
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2324
+ sanitized[key] = omitPrototypePollutionKeysDeep(entry);
2325
+ }
2326
+ return sanitized;
2327
+ }
2328
+ // src/utils/jsonc.ts
2329
+ /**
2330
+ * Rebuild the parsed value from its own enumerable entries, dropping
2331
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
2332
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
2333
+ * literal `__proto__` key would replace the containing object's prototype
2334
+ * instead of becoming an own property; rebuilding gives every object a clean
2335
+ * `Object.prototype` again and severs that path.
2336
+ */
2337
+ function deepSanitize(value) {
2338
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
2339
+ if (value !== null && typeof value === "object") {
2340
+ const sanitized = {};
2341
+ for (const [key, entry] of Object.entries(value)) {
2342
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2343
+ sanitized[key] = deepSanitize(entry);
2344
+ }
2345
+ return sanitized;
2346
+ }
2347
+ return value;
2348
+ }
2349
+ /**
2350
+ * Collect the paths of every prototype-pollution key in the document, walking
2351
+ * the syntax tree rather than the parsed value.
2352
+ *
2353
+ * The parsed value cannot answer this question for `__proto__`:
2354
+ * `obj["__proto__"] = "deny"` is discarded by the engine outright, and
2355
+ * `obj["__proto__"] = {...}` replaces the prototype instead of adding an own
2356
+ * property — either way the key is already gone by the time
2357
+ * {@link deepSanitize} could look for it, and only the source text still knows
2358
+ * the user wrote it. `constructor` and `prototype` do become own properties and
2359
+ * could be found in the parsed value, but they are collected here too so all
2360
+ * three keys are answered the same way.
2361
+ *
2362
+ * A pollution key's subtree is not descended into, matching
2363
+ * {@link deepSanitize}, which drops the whole value along with the key.
2364
+ */
2365
+ function collectPollutionKeyPaths({ node, path, found }) {
2366
+ if (node.type === "array") {
2367
+ for (const [index, child] of (node.children ?? []).entries()) collectPollutionKeyPaths({
2368
+ node: child,
2369
+ path: `${path}[${index}]`,
2370
+ found
2371
+ });
2372
+ return;
2373
+ }
2374
+ if (node.type !== "object") return;
2375
+ for (const property of node.children ?? []) {
2376
+ const [keyNode, valueNode] = property.children ?? [];
2377
+ if (typeof keyNode?.value !== "string") continue;
2378
+ const keyPath = path === "" ? keyNode.value : `${path}.${keyNode.value}`;
2379
+ if (PROTOTYPE_POLLUTION_KEYS.has(keyNode.value)) {
2380
+ found.push(keyPath);
2381
+ continue;
2382
+ }
2383
+ if (valueNode !== void 0) collectPollutionKeyPaths({
2384
+ node: valueNode,
2385
+ path: keyPath,
2386
+ found
2387
+ });
2388
+ }
2389
+ }
2390
+ /**
2391
+ * Parse a JSONC (JSON with Comments) document strictly.
2392
+ *
2393
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
2394
+ * errors and returns a best-effort value), this throws on any parse error so
2395
+ * a malformed source file fails loudly instead of generating half-empty tool
2396
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
2397
+ * `JSON.parse` on files that may contain comments or trailing commas.
2398
+ */
2399
+ function parseJsonc(content) {
2400
+ return deepSanitize(parseStrict(content));
2401
+ }
2402
+ /**
2403
+ * The strict parse shared by both entry points. Kept separate from the syntax
2404
+ * tree walk so the many callers that never look at `droppedKeys` do not pay
2405
+ * for building a second representation of the document.
2406
+ */
2407
+ function parseStrict(content) {
2408
+ const errors = [];
2409
+ const result = parse$1(content, errors, {
2410
+ allowTrailingComma: true,
2411
+ disallowComments: false
2412
+ });
2413
+ if (errors.length > 0) {
2414
+ const details = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
2415
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
2416
+ }
2417
+ return result;
2418
+ }
2419
+ /**
2420
+ * The error a source document should fail with when
2421
+ * {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
2422
+ *
2423
+ * Shared by every source that reports them, so the three files a user can
2424
+ * author explain the same removal the same way rather than each inventing its
2425
+ * own wording.
2426
+ */
2427
+ function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
2428
+ return /* @__PURE__ */ new Error(`${JSON.stringify(sourcePath)} uses ${droppedKeys.map((key) => JSON.stringify(key)).join(", ")} as ${droppedKeys.length === 1 ? "a key" : "keys"}. Rulesync removes __proto__, constructor and prototype from every source document it parses, because assigning them would reach the prototype chain instead of the object. They are therefore never written to any tool's config — rename them rather than leaving entries that silently do nothing.`);
2429
+ }
2430
+ /**
2431
+ * The same parse as {@link parseJsonc}, additionally reporting which
2432
+ * prototype-pollution keys were removed, as dotted paths
2433
+ * (`permission.bash.__proto__`).
2434
+ *
2435
+ * The parse itself is identical — the paths only let a caller tell the user
2436
+ * about entries that vanished, which they would otherwise have to notice by
2437
+ * comparing their source file against the generated output.
2438
+ */
2439
+ function parseJsoncReportingDroppedKeys({ content }) {
2440
+ const result = parseStrict(content);
2441
+ const found = [];
2442
+ const tree = parseTree(content, [], {
2443
+ allowTrailingComma: true,
2444
+ disallowComments: false
2445
+ });
2446
+ if (tree !== void 0) collectPollutionKeyPaths({
2447
+ node: tree,
2448
+ path: "",
2449
+ found
2450
+ });
2451
+ return {
2452
+ value: deepSanitize(result),
2453
+ droppedKeys: [...new Set(found)]
2454
+ };
2455
+ }
2272
2456
  // src/types/json-output.ts
2273
2457
  /**
2274
2458
  * Error codes for CLI errors
@@ -3558,10 +3742,37 @@ const getDefaults = () => ({
3558
3742
  inputRoots: void 0,
3559
3743
  sources: []
3560
3744
  });
3745
+ /**
3746
+ * Parse the content of a config file (`rulesync.jsonc` / `rulesync.local.jsonc`)
3747
+ * through the same strict, sanitizing JSONC parser as every other rulesync
3748
+ * source.
3749
+ *
3750
+ * `jsonc-parser`'s bare `parse` recovers from syntax errors without reporting
3751
+ * them, which let a typo (a missing comma, unquoted keys, trailing garbage)
3752
+ * silently drop keys and run rulesync with a different config than the user
3753
+ * wrote. Prototype-pollution keys are rejected rather than silently removed,
3754
+ * matching the hooks, MCP and permissions sources.
3755
+ */
3756
+ function parseConfigFileContent({ content, filePath }) {
3757
+ let result;
3758
+ try {
3759
+ result = parseJsoncReportingDroppedKeys({ content });
3760
+ } catch (error) {
3761
+ const detail = error instanceof Error ? error.message : String(error);
3762
+ throw new Error(`Failed to parse ${JSON.stringify(filePath)}: ${detail}`, { cause: error });
3763
+ }
3764
+ if (result.droppedKeys.length > 0) throw droppedPollutionKeysError({
3765
+ sourcePath: filePath,
3766
+ droppedKeys: result.droppedKeys
3767
+ });
3768
+ return result.value;
3769
+ }
3561
3770
  const loadConfigFromFile = async (filePath) => {
3562
3771
  if (!await fileExists(filePath)) return {};
3563
- const fileContent = await readFileContent(filePath);
3564
- const jsonData = parse$1(fileContent);
3772
+ const jsonData = parseConfigFileContent({
3773
+ content: await readFileContent(filePath),
3774
+ filePath
3775
+ });
3565
3776
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
3566
3777
  assertTargetsFeaturesExclusive({
3567
3778
  targets: configParams.targets,
@@ -4174,62 +4385,6 @@ function createBoundedWalk({ subject, limits, root }) {
4174
4385
  leave
4175
4386
  };
4176
4387
  }
4177
- // src/utils/prototype-pollution.ts
4178
- /**
4179
- * Keys that, if walked into when constructing or merging objects from
4180
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
4181
- * chain) and propagate state to every other object in the runtime. Any code
4182
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
4183
- * parsing, MCP config conversion, settings round-trip — should skip these.
4184
- */
4185
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
4186
- "__proto__",
4187
- "constructor",
4188
- "prototype"
4189
- ]);
4190
- function isPrototypePollutionKey(key) {
4191
- return PROTOTYPE_POLLUTION_KEYS.has(key);
4192
- }
4193
- /**
4194
- * Returns a shallow copy of a record's own entries with every
4195
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
4196
- *
4197
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
4198
- * or `headers` table — into freshly generated config. Carrying such a map by
4199
- * reference, or re-assigning its keys via bracket notation, would let a literal
4200
- * `__proto__` key ride along (and re-assigning it would mutate the target's
4201
- * prototype). Walking the entries through this helper severs that path while
4202
- * preserving every legitimate key.
4203
- */
4204
- function omitPrototypePollutionKeys(record) {
4205
- const sanitized = {};
4206
- for (const [key, value] of Object.entries(record)) {
4207
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4208
- sanitized[key] = value;
4209
- }
4210
- return sanitized;
4211
- }
4212
- /**
4213
- * Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
4214
- * reachable through the value (directly, or through an array) is copied with
4215
- * its prototype-pollution keys dropped. Scalars and arrays of scalars come back
4216
- * as they are.
4217
- *
4218
- * Use when passing an unknown, user-supplied value through unchanged — an MCP
4219
- * server field a tool documents that rulesync does not model — so a
4220
- * `__proto__` key nested anywhere inside it cannot ride into the generated
4221
- * config, where the consuming tool may merge the object without the same care.
4222
- */
4223
- function omitPrototypePollutionKeysDeep(value) {
4224
- if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
4225
- if (value === null || typeof value !== "object") return value;
4226
- const sanitized = {};
4227
- for (const [key, entry] of Object.entries(value)) {
4228
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4229
- sanitized[key] = omitPrototypePollutionKeysDeep(entry);
4230
- }
4231
- return sanitized;
4232
- }
4233
4388
  // src/utils/type-guards.ts
4234
4389
  /**
4235
4390
  * Type guard to check if a value is a plain object (Record<string, unknown>).
@@ -4857,6 +5012,24 @@ function lookupOwn({ record, key }) {
4857
5012
  return Object.hasOwn(record, key) ? record[key] : void 0;
4858
5013
  }
4859
5014
  /**
5015
+ * Return the value a map holds under its own `key`, first storing `create()`
5016
+ * there when it has none. A bracket read would find an inherited member for a
5017
+ * key such as `toString`, so an importer bucketing rules by a user-supplied
5018
+ * name would write into an `Object.prototype` function instead of a fresh
5019
+ * bucket.
5020
+ */
5021
+ function getOrCreateOwn({ record, key, create }) {
5022
+ if (Object.hasOwn(record, key)) return record[key];
5023
+ const created = create();
5024
+ Object.defineProperty(record, key, {
5025
+ value: created,
5026
+ writable: true,
5027
+ enumerable: true,
5028
+ configurable: true
5029
+ });
5030
+ return created;
5031
+ }
5032
+ /**
4860
5033
  * Swap the keys and values of a name map, e.g. a canonical-to-tool map into
4861
5034
  * the tool-to-canonical map an importer reads. When two keys share a value,
4862
5035
  * the later key wins.
@@ -6391,14 +6564,16 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
6391
6564
  * Hook events supported by Goose.
6392
6565
  *
6393
6566
  * Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
6394
- * maps PascalCase event names to matcher/handler arrays. Every Goose event has a
6395
- * 1:1 canonical equivalent, so no new canonical events are required.
6396
- *
6397
- * Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
6398
- * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
6399
- * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
6567
+ * maps PascalCase event names to matcher/handler arrays.
6568
+ *
6569
+ * Goose's `HookEvent` enum defines these 11 events plus `PreToolUseResult`
6570
+ * (added in v1.48.0), which has no canonical equivalent: it is authored under
6571
+ * the `goose.hooks` override block by its native name, emitted verbatim, and
6572
+ * imported back into that block. Notably the enum has NO
6573
+ * `SubagentStart`/`SubagentStop` arms — emitting them would write keys Goose
6574
+ * silently ignores, so `subagentStart`/`subagentStop` are intentionally
6400
6575
  * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
6401
- * @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
6576
+ * @see https://github.com/aaif-goose/goose/blob/v1.48.0/crates/goose/src/hooks/mod.rs
6402
6577
  * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
6403
6578
  */
6404
6579
  const GOOSE_HOOK_EVENTS = eventsOf(CANONICAL_TO_GOOSE_EVENT_NAMES);
@@ -6876,134 +7051,6 @@ const GROKCLI_HOOK_EVENTS = eventsOf(CANONICAL_TO_GROKCLI_EVENT_NAMES);
6876
7051
  * Map Grok CLI PascalCase event names to canonical camelCase.
6877
7052
  */
6878
7053
  const GROKCLI_TO_CANONICAL_EVENT_NAMES = invertRecord(CANONICAL_TO_GROKCLI_EVENT_NAMES);
6879
- // src/utils/jsonc.ts
6880
- /**
6881
- * Rebuild the parsed value from its own enumerable entries, dropping
6882
- * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
6883
- * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
6884
- * literal `__proto__` key would replace the containing object's prototype
6885
- * instead of becoming an own property; rebuilding gives every object a clean
6886
- * `Object.prototype` again and severs that path.
6887
- */
6888
- function deepSanitize(value) {
6889
- if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
6890
- if (value !== null && typeof value === "object") {
6891
- const sanitized = {};
6892
- for (const [key, entry] of Object.entries(value)) {
6893
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6894
- sanitized[key] = deepSanitize(entry);
6895
- }
6896
- return sanitized;
6897
- }
6898
- return value;
6899
- }
6900
- /**
6901
- * Collect the paths of every prototype-pollution key in the document, walking
6902
- * the syntax tree rather than the parsed value.
6903
- *
6904
- * The parsed value cannot answer this question for `__proto__`:
6905
- * `obj["__proto__"] = "deny"` is discarded by the engine outright, and
6906
- * `obj["__proto__"] = {...}` replaces the prototype instead of adding an own
6907
- * property — either way the key is already gone by the time
6908
- * {@link deepSanitize} could look for it, and only the source text still knows
6909
- * the user wrote it. `constructor` and `prototype` do become own properties and
6910
- * could be found in the parsed value, but they are collected here too so all
6911
- * three keys are answered the same way.
6912
- *
6913
- * A pollution key's subtree is not descended into, matching
6914
- * {@link deepSanitize}, which drops the whole value along with the key.
6915
- */
6916
- function collectPollutionKeyPaths({ node, path, found }) {
6917
- if (node.type === "array") {
6918
- for (const [index, child] of (node.children ?? []).entries()) collectPollutionKeyPaths({
6919
- node: child,
6920
- path: `${path}[${index}]`,
6921
- found
6922
- });
6923
- return;
6924
- }
6925
- if (node.type !== "object") return;
6926
- for (const property of node.children ?? []) {
6927
- const [keyNode, valueNode] = property.children ?? [];
6928
- if (typeof keyNode?.value !== "string") continue;
6929
- const keyPath = path === "" ? keyNode.value : `${path}.${keyNode.value}`;
6930
- if (PROTOTYPE_POLLUTION_KEYS.has(keyNode.value)) {
6931
- found.push(keyPath);
6932
- continue;
6933
- }
6934
- if (valueNode !== void 0) collectPollutionKeyPaths({
6935
- node: valueNode,
6936
- path: keyPath,
6937
- found
6938
- });
6939
- }
6940
- }
6941
- /**
6942
- * Parse a JSONC (JSON with Comments) document strictly.
6943
- *
6944
- * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
6945
- * errors and returns a best-effort value), this throws on any parse error so
6946
- * a malformed source file fails loudly instead of generating half-empty tool
6947
- * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
6948
- * `JSON.parse` on files that may contain comments or trailing commas.
6949
- */
6950
- function parseJsonc(content) {
6951
- return deepSanitize(parseStrict(content));
6952
- }
6953
- /**
6954
- * The strict parse shared by both entry points. Kept separate from the syntax
6955
- * tree walk so the many callers that never look at `droppedKeys` do not pay
6956
- * for building a second representation of the document.
6957
- */
6958
- function parseStrict(content) {
6959
- const errors = [];
6960
- const result = parse$1(content, errors, {
6961
- allowTrailingComma: true,
6962
- disallowComments: false
6963
- });
6964
- if (errors.length > 0) {
6965
- const details = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
6966
- throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
6967
- }
6968
- return result;
6969
- }
6970
- /**
6971
- * The error a source document should fail with when
6972
- * {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
6973
- *
6974
- * Shared by every source that reports them, so the three files a user can
6975
- * author explain the same removal the same way rather than each inventing its
6976
- * own wording.
6977
- */
6978
- function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
6979
- return /* @__PURE__ */ new Error(`${JSON.stringify(sourcePath)} uses ${droppedKeys.map((key) => JSON.stringify(key)).join(", ")} as ${droppedKeys.length === 1 ? "a key" : "keys"}. Rulesync removes __proto__, constructor and prototype from every source document it parses, because assigning them would reach the prototype chain instead of the object. They are therefore never written to any tool's config — rename them rather than leaving entries that silently do nothing.`);
6980
- }
6981
- /**
6982
- * The same parse as {@link parseJsonc}, additionally reporting which
6983
- * prototype-pollution keys were removed, as dotted paths
6984
- * (`permission.bash.__proto__`).
6985
- *
6986
- * The parse itself is identical — the paths only let a caller tell the user
6987
- * about entries that vanished, which they would otherwise have to notice by
6988
- * comparing their source file against the generated output.
6989
- */
6990
- function parseJsoncReportingDroppedKeys({ content }) {
6991
- const result = parseStrict(content);
6992
- const found = [];
6993
- const tree = parseTree(content, [], {
6994
- allowTrailingComma: true,
6995
- disallowComments: false
6996
- });
6997
- if (tree !== void 0) collectPollutionKeyPaths({
6998
- node: tree,
6999
- path: "",
7000
- found
7001
- });
7002
- return {
7003
- value: deepSanitize(result),
7004
- droppedKeys: [...new Set(found)]
7005
- };
7006
- }
7007
7054
  // src/utils/rulesync-source-path.ts
7008
7055
  /**
7009
7056
  * No `.rulesync/` source file exists for a feature at any of its candidate
@@ -19558,9 +19605,6 @@ var KiloCommand = class extends FrontmatterMarkdownToolCommand {
19558
19605
  static getFrontmatterSchema() {
19559
19606
  return KiloCommandFrontmatterSchema;
19560
19607
  }
19561
- getRulesyncOutputRoot() {
19562
- return process.cwd();
19563
- }
19564
19608
  /**
19565
19609
  * Refuse to write a command Kilo reserves (see
19566
19610
  * {@link KILO_RESERVED_COMMAND_NAMES}). The name Kilo loads is the file
@@ -20849,9 +20893,6 @@ var OpenCodeCommand = class extends FrontmatterMarkdownToolCommand {
20849
20893
  ...Object.keys(restFields).length > 0 && { [toolTarget]: restFields }
20850
20894
  };
20851
20895
  }
20852
- getRulesyncOutputRoot() {
20853
- return process.cwd();
20854
- }
20855
20896
  /**
20856
20897
  * Imports commands defined inline in `opencode.json` / `opencode.jsonc` (and,
20857
20898
  * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
@@ -58608,12 +58649,11 @@ var GooseHooks = class GooseHooks extends ToolHooks {
58608
58649
  });
58609
58650
  }
58610
58651
  toRulesyncHooks({ logger } = {}) {
58611
- const parsed = this.parseFileContentOrThrow({
58612
- displayName: "Goose",
58613
- parse: (fileContent) => JSON.parse(fileContent)
58614
- });
58615
58652
  const hooks = toolHooksToCanonical({
58616
- hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
58653
+ hooks: this.parseFileContentOrThrow({
58654
+ displayName: "Goose",
58655
+ parse: (fileContent) => JSON.parse(fileContent)
58656
+ }).hooks,
58617
58657
  converterConfig: GOOSE_CONVERTER_CONFIG,
58618
58658
  logger
58619
58659
  });
@@ -61880,12 +61920,14 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
61880
61920
  const paths = ReasonixHooks.getSettablePaths({ global });
61881
61921
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
61882
61922
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
61883
- let settings;
61923
+ let parsed;
61884
61924
  try {
61885
- settings = JSON.parse(existingContent);
61925
+ parsed = JSON.parse(existingContent);
61886
61926
  } catch (error) {
61887
61927
  throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
61888
61928
  }
61929
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: expected a mapping at the root`);
61930
+ const settings = parsed;
61889
61931
  const config = rulesyncHooks.getJson();
61890
61932
  const reasonixHooks = canonicalToReasonixHooks({
61891
61933
  config,
@@ -62844,15 +62886,18 @@ function emittedEventsWithHook(params) {
62844
62886
  }
62845
62887
  /**
62846
62888
  * The capabilities an adapter built on the shared hooks converter already
62847
- * declares in its converter config: the events it emits and the hook types it
62848
- * keeps. Reading them from there keeps the processor's skip warnings in step
62849
- * with what the converter actually writes.
62889
+ * declares in its converter config: the events it emits, the hook types it
62890
+ * keeps, and whether it writes the per-hook `enabled` flag. Reading them from
62891
+ * there keeps the processor's skip and dropped-field warnings in step with
62892
+ * what the converter actually writes.
62850
62893
  */
62851
62894
  function converterCapabilities(config) {
62852
62895
  if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
62896
+ const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
62853
62897
  return {
62854
62898
  supportedEvents: config.supportedEvents,
62855
- supportedHookTypes: [...config.supportedHookTypes]
62899
+ supportedHookTypes: [...config.supportedHookTypes],
62900
+ ...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
62856
62901
  };
62857
62902
  }
62858
62903
  const toolHooksFactories = /* @__PURE__ */ new Map([
@@ -63474,8 +63519,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
63474
63519
  supportsImport: true
63475
63520
  },
63476
63521
  ...converterCapabilities(ZCODE_CONVERTER_CONFIG),
63477
- supportsMatcher: true,
63478
- supportsPerHookEnabled: true
63522
+ supportsMatcher: true
63479
63523
  }],
63480
63524
  ["zcode-plugin", {
63481
63525
  class: ZcodePluginHooks,
@@ -63485,10 +63529,12 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
63485
63529
  supportsImport: true
63486
63530
  },
63487
63531
  ...converterCapabilities(ZCODE_PLUGIN_CONVERTER_CONFIG),
63488
- supportsMatcher: true,
63489
- supportsPerHookEnabled: true
63532
+ supportsMatcher: true
63490
63533
  }]
63491
63534
  ]);
63535
+ function targetsSupporting(capability) {
63536
+ return [...toolHooksFactories.entries()].filter(([, f]) => f[capability]).map(([t]) => t).join(" / ");
63537
+ }
63492
63538
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
63493
63539
  const hooksProcessorToolTargetsGlobal = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsGlobal).map(([t]) => t);
63494
63540
  const hooksProcessorToolTargetsImportable = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject && f.meta.supportsImport).map(([t]) => t);
@@ -63649,7 +63695,7 @@ var HooksProcessor = class extends FeatureProcessor {
63649
63695
  effectiveHooks,
63650
63696
  predicate: (def) => def.enabled === false
63651
63697
  });
63652
- if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide / zcode hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
63698
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the ${targetsSupporting("supportsPerHookEnabled")} hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
63653
63699
  }
63654
63700
  if (!factory.supportsPerHookConfirm) {
63655
63701
  const eventsWithConfirmHooks = emittedEventsWithHook({
@@ -63658,7 +63704,7 @@ var HooksProcessor = class extends FeatureProcessor {
63658
63704
  effectiveHooks,
63659
63705
  predicate: (def) => def.confirm !== void 0
63660
63706
  });
63661
- if (eventsWithConfirmHooks.length > 0) this.logger.warn(`Emitting hook(s) without their "confirm" prompt for ${this.toolTarget} (only the kiro-cli / kiro-ide hooks formats support it): ${eventsWithConfirmHooks.join(", ")}`);
63707
+ if (eventsWithConfirmHooks.length > 0) this.logger.warn(`Emitting hook(s) without their "confirm" prompt for ${this.toolTarget} (only the ${targetsSupporting("supportsPerHookConfirm")} hooks formats support it): ${eventsWithConfirmHooks.join(", ")}`);
63662
63708
  }
63663
63709
  }
63664
63710
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -64921,12 +64967,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
64921
64967
  }
64922
64968
  toRulesyncIgnore() {
64923
64969
  const rulesyncPatterns = uniq(this.patterns.map((entry) => readDenyPatternOf(entry)).filter((pattern) => pattern !== null && pattern.length > 0));
64924
- return new RulesyncIgnore({
64925
- outputRoot: this.outputRoot,
64926
- relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
64927
- relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
64928
- fileContent: rulesyncPatterns.join("\n")
64929
- });
64970
+ return this.toRulesyncIgnoreDefault({ fileContent: rulesyncPatterns.join("\n") });
64930
64971
  }
64931
64972
  static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
64932
64973
  await warnIfReasonixNestedProjectConfigShadowed({
@@ -78633,11 +78674,17 @@ function convertAmpToRulesync({ disable, permissions }) {
78633
78674
  const assign = (tool, pattern, action) => {
78634
78675
  if (isPrototypePollutionKey(tool)) return;
78635
78676
  if (isPrototypePollutionKey(pattern)) return;
78636
- const bucket = permission[tool] ??= {};
78637
- const existing = bucket[pattern];
78677
+ const bucket = getOrCreateOwn({
78678
+ record: permission,
78679
+ key: tool,
78680
+ create: () => ({})
78681
+ });
78638
78682
  if (isStricterAction({
78639
78683
  action,
78640
- existing
78684
+ existing: lookupOwn({
78685
+ record: bucket,
78686
+ key: pattern
78687
+ })
78641
78688
  })) bucket[pattern] = action;
78642
78689
  };
78643
78690
  for (const tool of disable) assign(tool, "*", "deny");
@@ -80040,8 +80087,12 @@ function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory,
80040
80087
  logger,
80041
80088
  toolLabel
80042
80089
  }) : target;
80043
- if (!permission[canonical]) permission[canonical] = {};
80044
- permission[canonical][pattern] = action;
80090
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
80091
+ getOrCreateOwn({
80092
+ record: permission,
80093
+ key: canonical,
80094
+ create: () => ({})
80095
+ })[pattern] = action;
80045
80096
  }
80046
80097
  };
80047
80098
  processEntries(allow, "allow");
@@ -80109,10 +80160,16 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
80109
80160
  };
80110
80161
  function toAntigravityCliToolName(canonical) {
80111
80162
  if (canonical.startsWith("mcp__")) return "mcp";
80112
- return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
80163
+ return lookupOwn({
80164
+ record: CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES,
80165
+ key: canonical
80166
+ }) ?? canonical;
80113
80167
  }
80114
80168
  function toCanonicalToolName$9(cliName) {
80115
- return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
80169
+ return lookupOwn({
80170
+ record: ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES,
80171
+ key: cliName
80172
+ }) ?? cliName;
80116
80173
  }
80117
80174
  /**
80118
80175
  * Permissions generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
@@ -80275,10 +80332,16 @@ const IDE_ACTION_TO_CANONICAL = {
80275
80332
  };
80276
80333
  function toIdeAction(canonical) {
80277
80334
  if (canonical.startsWith("mcp__")) return "mcp";
80278
- return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
80335
+ return lookupOwn({
80336
+ record: CANONICAL_TO_IDE_ACTION,
80337
+ key: canonical
80338
+ }) ?? canonical;
80279
80339
  }
80280
80340
  function toCanonicalCategory$2(ideAction) {
80281
- return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
80341
+ return lookupOwn({
80342
+ record: IDE_ACTION_TO_CANONICAL,
80343
+ key: ideAction
80344
+ }) ?? ideAction;
80282
80345
  }
80283
80346
  /**
80284
80347
  * Permissions generator for the Google Antigravity IDE (Antigravity 2.0).
@@ -81817,10 +81880,16 @@ const CANONICAL_TO_CLAUDE_TOOL_NAMES = {
81817
81880
  */
81818
81881
  const CLAUDE_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_CLAUDE_TOOL_NAMES);
81819
81882
  function toClaudeToolName(canonical) {
81820
- return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
81883
+ return lookupOwn({
81884
+ record: CANONICAL_TO_CLAUDE_TOOL_NAMES,
81885
+ key: canonical
81886
+ }) ?? canonical;
81821
81887
  }
81822
81888
  function toCanonicalToolName$7(claudeName) {
81823
- return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
81889
+ return lookupOwn({
81890
+ record: CLAUDE_TO_CANONICAL_TOOL_NAMES,
81891
+ key: claudeName
81892
+ }) ?? claudeName;
81824
81893
  }
81825
81894
  /**
81826
81895
  * Merge `patch` into `base`, recursing into plain objects so a sibling key at
@@ -82792,7 +82861,10 @@ const CLAUDE_PATH_RULE_ALIASES = {
82792
82861
  */
82793
82862
  function buildClaudePermissionEntry(toolName, pattern) {
82794
82863
  if (pattern === "*") return toolName;
82795
- return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
82864
+ return `${lookupOwn({
82865
+ record: CLAUDE_PATH_RULE_ALIASES,
82866
+ key: toolName
82867
+ }) ?? toolName}(${pattern})`;
82796
82868
  }
82797
82869
  /**
82798
82870
  * The Claude tool names the canonical config manages. Deliberately the tool
@@ -83030,8 +83102,12 @@ function convertClaudeToRulesyncPermissions(params) {
83030
83102
  for (const entry of entries) {
83031
83103
  const { toolName, pattern } = parseToolPatternEntry(entry);
83032
83104
  const canonical = toCanonicalToolName$7(toolName);
83033
- if (!permission[canonical]) permission[canonical] = {};
83034
- permission[canonical][pattern] = action;
83105
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
83106
+ getOrCreateOwn({
83107
+ record: permission,
83108
+ key: canonical,
83109
+ create: () => ({})
83110
+ })[pattern] = action;
83035
83111
  }
83036
83112
  };
83037
83113
  processEntries(params.allow, "allow");
@@ -83613,11 +83689,17 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
83613
83689
  for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringArray$1(permissions[action])) {
83614
83690
  const parsed = parseCodebuddyRule(rule);
83615
83691
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
83616
- const rules = permission[parsed.category] ??= {};
83617
- const existing = rules[parsed.pattern];
83692
+ const rules = getOrCreateOwn({
83693
+ record: permission,
83694
+ key: parsed.category,
83695
+ create: () => ({})
83696
+ });
83618
83697
  if (isStricterAction({
83619
83698
  action,
83620
- existing
83699
+ existing: lookupOwn({
83700
+ record: rules,
83701
+ key: parsed.pattern
83702
+ })
83621
83703
  })) rules[parsed.pattern] = action;
83622
83704
  }
83623
83705
  const codebuddy = liftCodebuddyOverride({
@@ -84127,7 +84209,10 @@ const CODEWHALE_WRITE_RESTRICTION_TOOLS = ["write_file", "apply_patch"];
84127
84209
  /** The Codewhale tools a canonical path rule is written for. */
84128
84210
  function pathCategoryTools(category, action) {
84129
84211
  if (category === "write" && action !== "allow") return CODEWHALE_WRITE_RESTRICTION_TOOLS;
84130
- return CODEWHALE_PATH_CATEGORY_TOOLS[category] ?? [];
84212
+ return lookupOwn({
84213
+ record: CODEWHALE_PATH_CATEGORY_TOOLS,
84214
+ key: category
84215
+ }) ?? [];
84131
84216
  }
84132
84217
  /** The canonical path categories a Codewhale file tool's rule can come from. */
84133
84218
  const CODEWHALE_TOOL_TO_PATH_CATEGORIES = {
@@ -84446,7 +84531,10 @@ function canonicalToCodewhaleRules({ config, preservedRules, logger }) {
84446
84531
  });
84447
84532
  for (const [category, rules] of Object.entries(config.permission)) {
84448
84533
  if (category === "bash" || category === "*") continue;
84449
- if (CODEWHALE_PATH_CATEGORY_TOOLS[category] === void 0) {
84534
+ if (lookupOwn({
84535
+ record: CODEWHALE_PATH_CATEGORY_TOOLS,
84536
+ key: category
84537
+ }) === void 0) {
84450
84538
  if (Object.keys(rules).length > 0) logger?.warn(`Codewhale permissions: skipping unsupported category "${category}"; Codewhale evaluates permission rules only for shell commands and file tools.`);
84451
84539
  continue;
84452
84540
  }
@@ -84556,7 +84644,10 @@ function toImportCandidates(rule) {
84556
84644
  }
84557
84645
  const pattern = pathRuleToPattern(rule);
84558
84646
  if (pattern === null) return [];
84559
- return (CODEWHALE_TOOL_TO_PATH_CATEGORIES[rule.tool] ?? []).map((category) => ({
84647
+ return (lookupOwn({
84648
+ record: CODEWHALE_TOOL_TO_PATH_CATEGORIES,
84649
+ key: rule.tool
84650
+ }) ?? []).map((category) => ({
84560
84651
  category,
84561
84652
  pattern,
84562
84653
  rule
@@ -86164,10 +86255,17 @@ function parseCommandcodeRuleLists(permissions) {
86164
86255
  rule,
86165
86256
  category
86166
86257
  })) fallbackLogger.warn(`Command Code matches '${rule}' in "${action}" against MCP tool names case-insensitively, but the '${category}' rule generated from it matches the server and tool names exactly as written; spell the rule the way the tool is registered if they differ.`);
86167
- const rules = permission[category] ??= {};
86258
+ const rules = getOrCreateOwn({
86259
+ record: permission,
86260
+ key: category,
86261
+ create: () => ({})
86262
+ });
86168
86263
  if (isStricterAction({
86169
86264
  action,
86170
- existing: rules[pattern]
86265
+ existing: lookupOwn({
86266
+ record: rules,
86267
+ key: pattern
86268
+ })
86171
86269
  })) rules[pattern] = action;
86172
86270
  }
86173
86271
  }
@@ -86528,9 +86626,12 @@ function convertContinuePermissionsToRulesync(lists) {
86528
86626
  const parsedEntry = parseContinuePermissionEntry(entry);
86529
86627
  if (parsedEntry === void 0) continue;
86530
86628
  if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
86531
- const category = toCanonicalToolName$6(parsedEntry.toolName);
86532
- permission[category] ??= {};
86533
- permission[category][parsedEntry.pattern] ??= action;
86629
+ const rules = getOrCreateOwn({
86630
+ record: permission,
86631
+ key: toCanonicalToolName$6(parsedEntry.toolName),
86632
+ create: () => ({})
86633
+ });
86634
+ if (!Object.hasOwn(rules, parsedEntry.pattern)) rules[parsedEntry.pattern] = action;
86534
86635
  }
86535
86636
  }
86536
86637
  return { permission };
@@ -87159,7 +87260,9 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
87159
87260
  const settings = this.parseSettings();
87160
87261
  const rules = {};
87161
87262
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
87162
- if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
87263
+ if (this.global) {
87264
+ for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) if (!Object.hasOwn(rules, pattern)) rules[pattern] = "allow";
87265
+ }
87163
87266
  const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
87164
87267
  const defaultPermissionMode = this.global ? settings[DEFAULT_PERMISSION_MODE_KEY] : void 0;
87165
87268
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -87506,7 +87609,10 @@ function isMcpScopedCategory(canonical) {
87506
87609
  }
87507
87610
  function toCursorType(canonical) {
87508
87611
  if (isMcpScopedCategory(canonical)) return "Mcp";
87509
- return CANONICAL_TO_CURSOR_TYPE[canonical] ?? canonical;
87612
+ return lookupOwn({
87613
+ record: CANONICAL_TO_CURSOR_TYPE,
87614
+ key: canonical
87615
+ }) ?? canonical;
87510
87616
  }
87511
87617
  /**
87512
87618
  * For per-tool MCP canonical categories like `mcp__puppeteer__navigate`, the
@@ -87538,9 +87644,15 @@ function toCanonicalCategory$1(cursorType, pattern) {
87538
87644
  const tool = match[2] ?? "*";
87539
87645
  return `${MCP_CATEGORY_PREFIX}${server}__${tool}`;
87540
87646
  }
87541
- return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
87647
+ return lookupOwn({
87648
+ record: CURSOR_TYPE_TO_CANONICAL,
87649
+ key: cursorType
87650
+ }) ?? cursorType.toLowerCase();
87542
87651
  }
87543
- return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
87652
+ return lookupOwn({
87653
+ record: CURSOR_TYPE_TO_CANONICAL,
87654
+ key: cursorType
87655
+ }) ?? cursorType.toLowerCase();
87544
87656
  }
87545
87657
  /**
87546
87658
  * Narrow `JSON.parse` output to a plain object before treating it as a Cursor
@@ -87785,9 +87897,13 @@ function convertCursorToRulesyncPermissions(params) {
87785
87897
  for (const entry of entries) {
87786
87898
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
87787
87899
  const canonical = toCanonicalCategory$1(type, pattern);
87788
- if (!permission[canonical]) permission[canonical] = {};
87789
87900
  const canonicalPattern = type === "Mcp" && canonical.startsWith("mcp__") ? "*" : pattern;
87790
- permission[canonical][canonicalPattern] = action;
87901
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(canonicalPattern)) continue;
87902
+ getOrCreateOwn({
87903
+ record: permission,
87904
+ key: canonical,
87905
+ create: () => ({})
87906
+ })[canonicalPattern] = action;
87791
87907
  }
87792
87908
  };
87793
87909
  processEntries(params.allow, "allow");
@@ -88714,10 +88830,16 @@ const DEVIN_SCOPE_TO_CANONICAL = {
88714
88830
  */
88715
88831
  const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
88716
88832
  function toDevinScope(canonical) {
88717
- return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
88833
+ return lookupOwn({
88834
+ record: CANONICAL_TO_DEVIN_SCOPE,
88835
+ key: canonical
88836
+ }) ?? canonical;
88718
88837
  }
88719
88838
  function toCanonicalCategory(devinScope) {
88720
- return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
88839
+ return lookupOwn({
88840
+ record: DEVIN_SCOPE_TO_CANONICAL,
88841
+ key: devinScope
88842
+ }) ?? devinScope;
88721
88843
  }
88722
88844
  function asDevinRecord(value) {
88723
88845
  return isRecord(value) ? { ...value } : {};
@@ -89069,7 +89191,11 @@ function convertDevinToRulesyncPermissions(params) {
89069
89191
  const { toolName: scope, pattern } = parseToolPatternEntry(entry);
89070
89192
  if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
89071
89193
  const canonical = toCanonicalCategory(scope);
89072
- (permission[canonical] ??= {})[pattern] = action;
89194
+ getOrCreateOwn({
89195
+ record: permission,
89196
+ key: canonical,
89197
+ create: () => ({})
89198
+ })[pattern] = action;
89073
89199
  }
89074
89200
  };
89075
89201
  processEntries(params.allow, "allow");
@@ -89375,7 +89501,10 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
89375
89501
  };
89376
89502
  const assigned = /* @__PURE__ */ new Map();
89377
89503
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
89378
- const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
89504
+ const toolName = lookupOwn({
89505
+ record: RULESYNC_TO_GOOSE_TOOL_NAME,
89506
+ key: category
89507
+ }) ?? category;
89379
89508
  for (const [pattern, action] of Object.entries(rules)) if (pattern !== "*") logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
89380
89509
  const { action, downgraded } = failSafeCatchAllAction({
89381
89510
  rules,
@@ -89407,9 +89536,16 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
89407
89536
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
89408
89537
  const toolNames = isStringArray(userPermission[key]) ? userPermission[key] : [];
89409
89538
  const action = GOOSE_LIST_TO_ACTION[key];
89410
- for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
89411
- permission[category] ??= {};
89412
- permission[category]["*"] = action;
89539
+ for (const toolName of toolNames) for (const category of lookupOwn({
89540
+ record: GOOSE_TO_RULESYNC_TOOL_NAMES,
89541
+ key: toolName
89542
+ }) ?? [toolName]) {
89543
+ if (isPrototypePollutionKey(category)) continue;
89544
+ getOrCreateOwn({
89545
+ record: permission,
89546
+ key: category,
89547
+ create: () => ({})
89548
+ })["*"] = action;
89413
89549
  }
89414
89550
  }
89415
89551
  return { permission };
@@ -89490,7 +89626,10 @@ function buildGrokEntry(category, pattern) {
89490
89626
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
89491
89627
  }
89492
89628
  if (category === "mcp") return pattern === "*" || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
89493
- const tool = CATEGORY_TO_GROK_TOOL[category];
89629
+ const tool = lookupOwn({
89630
+ record: CATEGORY_TO_GROK_TOOL,
89631
+ key: category
89632
+ });
89494
89633
  if (tool === void 0) return null;
89495
89634
  return pattern === "*" || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
89496
89635
  }
@@ -89524,7 +89663,10 @@ function parseGrokEntry(entry) {
89524
89663
  category: "mcp",
89525
89664
  pattern: "*"
89526
89665
  };
89527
- const category = GROK_TOOL_TO_CATEGORY[tool];
89666
+ const category = lookupOwn({
89667
+ record: GROK_TOOL_TO_CATEGORY,
89668
+ key: tool
89669
+ });
89528
89670
  if (category === void 0) return null;
89529
89671
  return {
89530
89672
  category,
@@ -89845,11 +89987,17 @@ function parseGrokPermissionArrays(permission) {
89845
89987
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
89846
89988
  const result = {};
89847
89989
  const record = ({ category, pattern, action }) => {
89848
- const bucket = result[category] ??= {};
89849
- const existing = bucket[pattern];
89990
+ const bucket = getOrCreateOwn({
89991
+ record: result,
89992
+ key: category,
89993
+ create: () => ({})
89994
+ });
89850
89995
  if (isStricterAction({
89851
89996
  action,
89852
- existing
89997
+ existing: lookupOwn({
89998
+ record: bucket,
89999
+ key: pattern
90000
+ })
89853
90001
  })) bucket[pattern] = action;
89854
90002
  };
89855
90003
  const applyEntries = (entries, action) => {
@@ -89906,7 +90054,11 @@ function deleteRulesByAction(rules, action) {
89906
90054
  for (const [pattern, currentAction] of Object.entries(rules)) if (currentAction === action) delete rules[pattern];
89907
90055
  }
89908
90056
  function ensureCategory(permission, category) {
89909
- return permission[category] ??= {};
90057
+ return getOrCreateOwn({
90058
+ record: permission,
90059
+ key: category,
90060
+ create: () => ({})
90061
+ });
89910
90062
  }
89911
90063
  function removeEmptyCategories(permission) {
89912
90064
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
@@ -90549,7 +90701,10 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
90549
90701
  function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
90550
90702
  const ruleLists = {};
90551
90703
  for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
90552
- const group = CANONICAL_TO_JUNIE_GROUP[category];
90704
+ const group = lookupOwn({
90705
+ record: CANONICAL_TO_JUNIE_GROUP,
90706
+ key: category
90707
+ });
90553
90708
  if (!group) {
90554
90709
  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.`);
90555
90710
  continue;
@@ -90947,7 +91102,10 @@ const CATEGORY_TO_KIMI_CODE_TOOL = {
90947
91102
  function buildKimiCodePattern(category, pattern) {
90948
91103
  if (category.startsWith("mcp__")) return pattern === "*" || pattern === "" ? category : null;
90949
91104
  if (category === "mcp") return pattern === "*" || pattern === "" ? "mcp__*" : `mcp__${pattern}`;
90950
- const tool = CATEGORY_TO_KIMI_CODE_TOOL[category];
91105
+ const tool = lookupOwn({
91106
+ record: CATEGORY_TO_KIMI_CODE_TOOL,
91107
+ key: category
91108
+ });
90951
91109
  if (!tool) return null;
90952
91110
  return pattern === "*" || pattern === "" ? tool : `${tool}(${pattern})`;
90953
91111
  }
@@ -91043,7 +91201,10 @@ function canonicalToKimiCodeRules({ config, logger }) {
91043
91201
  const canonicalRules = [];
91044
91202
  const overrideRules = config["kimi-code"]?.rules ?? [];
91045
91203
  for (const [category, rules] of Object.entries(config.permission)) {
91046
- if (!(category === "mcp" || category.startsWith("mcp__") || CATEGORY_TO_KIMI_CODE_TOOL[category] !== void 0)) {
91204
+ if (!(category === "mcp" || category.startsWith("mcp__") || lookupOwn({
91205
+ record: CATEGORY_TO_KIMI_CODE_TOOL,
91206
+ key: category
91207
+ }) !== void 0)) {
91047
91208
  if (Object.keys(rules).length > 0) logger?.warn(`Kimi Code permissions: skipping unsupported category "${category}".`);
91048
91209
  continue;
91049
91210
  }
@@ -91860,9 +92021,16 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
91860
92021
  if (!isImportableRule(rule) || !isRecord(rule) || !isPermissionAction(rule.effect)) continue;
91861
92022
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
91862
92023
  const patterns = rule.match === void 0 ? ["*"] : isStringArray(rule.match) ? rule.match : [];
91863
- const categoryRules = permission[category] ??= {};
92024
+ const categoryRules = getOrCreateOwn({
92025
+ record: permission,
92026
+ key: category,
92027
+ create: () => ({})
92028
+ });
91864
92029
  for (const pattern of patterns) {
91865
- const previous = categoryRules[pattern];
92030
+ const previous = lookupOwn({
92031
+ record: categoryRules,
92032
+ key: pattern
92033
+ });
91866
92034
  if (isStricterAction({
91867
92035
  action: rule.effect,
91868
92036
  existing: previous
@@ -91880,7 +92048,10 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
91880
92048
  function buildKiroCapabilityRules({ config, logger }) {
91881
92049
  const buckets = /* @__PURE__ */ new Map();
91882
92050
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
91883
- const capability = CANONICAL_TO_KIRO_CAPABILITY[category];
92051
+ const capability = lookupOwn({
92052
+ record: CANONICAL_TO_KIRO_CAPABILITY,
92053
+ key: category
92054
+ });
91884
92055
  if (capability === void 0) {
91885
92056
  logger?.warn(`Kiro permissions.yaml has no capability for category: ${category}. Skipping.`);
91886
92057
  continue;
@@ -92040,7 +92211,10 @@ function toLettacodeBashPayload({ pattern, action, logger }) {
92040
92211
  return prefix === "" ? "" : `${prefix}${PREFIX_SUFFIX}`;
92041
92212
  }
92042
92213
  function toLettacodeRule({ category, pattern, action, logger }) {
92043
- const tool = CATEGORY_TO_LETTACODE_TOOL[category];
92214
+ const tool = lookupOwn({
92215
+ record: CATEGORY_TO_LETTACODE_TOOL,
92216
+ key: category
92217
+ });
92044
92218
  if (tool === void 0) return null;
92045
92219
  const payload = category === "bash" ? toLettacodeBashPayload({
92046
92220
  pattern,
@@ -92195,11 +92369,17 @@ var LettacodePermissions = class LettacodePermissions extends ToolPermissions {
92195
92369
  for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringArray$1(permissions[key])) {
92196
92370
  const parsed = fromLettacodeRule(rule);
92197
92371
  if (parsed === null || isPrototypePollutionKey(parsed.pattern)) continue;
92198
- const rules = permission[parsed.category] ??= {};
92199
- const existing = rules[parsed.pattern];
92372
+ const rules = getOrCreateOwn({
92373
+ record: permission,
92374
+ key: parsed.category,
92375
+ create: () => ({})
92376
+ });
92200
92377
  if (isStricterAction({
92201
92378
  action,
92202
- existing
92379
+ existing: lookupOwn({
92380
+ record: rules,
92381
+ key: parsed.pattern
92382
+ })
92203
92383
  })) rules[parsed.pattern] = action;
92204
92384
  }
92205
92385
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
@@ -92386,7 +92566,13 @@ function getOpencodeMcpServerNames(mcp) {
92386
92566
  return isRecord(lowered) ? Object.keys(lowered) : [];
92387
92567
  }
92388
92568
  function toOpencodePermissionKey(canonical) {
92389
- return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? CANONICAL_TO_OPENCODE_FOLDED_PERMISSION_KEYS[canonical] ?? canonical;
92569
+ return lookupOwn({
92570
+ record: CANONICAL_TO_OPENCODE_PERMISSION_KEYS,
92571
+ key: canonical
92572
+ }) ?? lookupOwn({
92573
+ record: CANONICAL_TO_OPENCODE_FOLDED_PERMISSION_KEYS,
92574
+ key: canonical
92575
+ }) ?? canonical;
92390
92576
  }
92391
92577
  /**
92392
92578
  * Merge two canonical pattern maps that land on the same OpenCode key.
@@ -92410,7 +92596,10 @@ function mergePatternRules(base, incoming) {
92410
92596
  return Object.fromEntries(Object.entries(merged).toSorted(([, a], [, b]) => PERMISSION_ACTION_PRIORITY[a] - PERMISSION_ACTION_PRIORITY[b]));
92411
92597
  }
92412
92598
  function toCanonicalPermissionKey(opencodeKey) {
92413
- return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
92599
+ return lookupOwn({
92600
+ record: OPENCODE_TO_CANONICAL_PERMISSION_KEYS,
92601
+ key: opencodeKey
92602
+ }) ?? opencodeKey;
92414
92603
  }
92415
92604
  function isSharedPermissionCategory(category) {
92416
92605
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES$1.has(category) || category.startsWith("mcp__");
@@ -93692,7 +93881,14 @@ function importPoolTool({ toolConfig, bucket }) {
93692
93881
  const denied = isStringArray(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
93693
93882
  const allowed = isStringArray(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
93694
93883
  for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
93695
- for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
93884
+ for (const pattern of allowed.filter(isUsablePattern)) if (!Object.hasOwn(bucket, pattern)) bucket[pattern] = "allow";
93885
+ }
93886
+ /**
93887
+ * Record `allow` for a path no deny has claimed. The check is on own
93888
+ * properties, so a path such as `toString` is not mistaken for a stored rule.
93889
+ */
93890
+ function allowUnlessSet(bucket, path) {
93891
+ if (!Object.hasOwn(bucket, path)) bucket[path] = "allow";
93696
93892
  }
93697
93893
  /**
93698
93894
  * Convert Pool's `tools` and `paths` blocks back into a rulesync config.
@@ -93727,8 +93923,8 @@ function convertPoolToRulesync({ tools, paths }) {
93727
93923
  for (const entry of allowEntries) {
93728
93924
  const path = pathOfEntry(entry);
93729
93925
  if (path === void 0) continue;
93730
- bucketFor(READ_CATEGORY)[path] ??= "allow";
93731
- if (isRecord(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
93926
+ allowUnlessSet(bucketFor(READ_CATEGORY), path);
93927
+ if (isRecord(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) allowUnlessSet(bucketFor(category), path);
93732
93928
  }
93733
93929
  return { permission };
93734
93930
  }
@@ -93857,7 +94053,10 @@ function toQoderRule({ category, pattern, logger }) {
93857
94053
  logger?.warn(`Qoder permissions: skipping the '${category}' rule '${pattern}' — Qoder matches MCP tools by name only.`);
93858
94054
  return null;
93859
94055
  }
93860
- const tool = CATEGORY_TO_QODER_TOOL[category];
94056
+ const tool = lookupOwn({
94057
+ record: CATEGORY_TO_QODER_TOOL,
94058
+ key: category
94059
+ });
93861
94060
  if (tool === void 0) return null;
93862
94061
  if (pattern === "*") return tool;
93863
94062
  return `${QODER_PATH_RULE_ALIASES[tool] ?? tool}(${pattern})`;
@@ -94143,11 +94342,17 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
94143
94342
  for (const action of ACTIONS) for (const rule of toStringArray$1(permissions[action])) {
94144
94343
  const parsed = fromQoderRule(rule);
94145
94344
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
94146
- const rules = permission[parsed.category] ??= {};
94147
- const existing = rules[parsed.pattern];
94345
+ const rules = getOrCreateOwn({
94346
+ record: permission,
94347
+ key: parsed.category,
94348
+ create: () => ({})
94349
+ });
94148
94350
  if (isStricterAction({
94149
94351
  action,
94150
- existing
94352
+ existing: lookupOwn({
94353
+ record: rules,
94354
+ key: parsed.pattern
94355
+ })
94151
94356
  })) rules[parsed.pattern] = action;
94152
94357
  }
94153
94358
  const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
@@ -94211,10 +94416,16 @@ const CANONICAL_TO_QWEN_TOOL_NAMES = {
94211
94416
  };
94212
94417
  const QWEN_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_QWEN_TOOL_NAMES);
94213
94418
  function toQwenToolName(canonical) {
94214
- return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
94419
+ return lookupOwn({
94420
+ record: CANONICAL_TO_QWEN_TOOL_NAMES,
94421
+ key: canonical
94422
+ }) ?? canonical;
94215
94423
  }
94216
94424
  function toCanonicalToolName$3(qwenName) {
94217
- return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
94425
+ return lookupOwn({
94426
+ record: QWEN_TO_CANONICAL_TOOL_NAMES,
94427
+ key: qwenName
94428
+ }) ?? qwenName;
94218
94429
  }
94219
94430
  function parseQwenPermissionEntry(entry, options = {}) {
94220
94431
  const parenIndex = entry.indexOf("(");
@@ -94830,15 +95041,23 @@ function convertQwenToRulesyncPermissions(params) {
94830
95041
  if (!parsed.ok) {
94831
95042
  if (action === "deny") {
94832
95043
  const canonical = toCanonicalToolName$3(parsed.toolName);
94833
- if (!permission[canonical]) permission[canonical] = {};
94834
- permission[canonical]["*"] = action;
95044
+ if (isPrototypePollutionKey(canonical)) continue;
95045
+ getOrCreateOwn({
95046
+ record: permission,
95047
+ key: canonical,
95048
+ create: () => ({})
95049
+ })["*"] = action;
94835
95050
  }
94836
95051
  continue;
94837
95052
  }
94838
95053
  const { toolName, pattern } = parsed;
94839
95054
  const canonical = toCanonicalToolName$3(toolName);
94840
- if (!permission[canonical]) permission[canonical] = {};
94841
- permission[canonical][pattern] = action;
95055
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
95056
+ getOrCreateOwn({
95057
+ record: permission,
95058
+ key: canonical,
95059
+ create: () => ({})
95060
+ })[pattern] = action;
94842
95061
  }
94843
95062
  };
94844
95063
  processEntries(params.allow, "allow");
@@ -94902,15 +95121,24 @@ const REASONIX_TO_CANONICAL_TOOL_NAMES = {
94902
95121
  };
94903
95122
  /** Every Reasonix tool name a canonical category owns on generate. */
94904
95123
  function reasonixToolNamesOf(canonical) {
94905
- const legacy = CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES[canonical];
95124
+ const legacy = lookupOwn({
95125
+ record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
95126
+ key: canonical
95127
+ });
94906
95128
  const current = toReasonixToolName(canonical);
94907
95129
  return legacy === void 0 ? [current] : [current, legacy];
94908
95130
  }
94909
95131
  function toReasonixToolName(canonical) {
94910
- return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
95132
+ return lookupOwn({
95133
+ record: CANONICAL_TO_REASONIX_TOOL_NAMES,
95134
+ key: canonical
95135
+ }) ?? canonical;
94911
95136
  }
94912
95137
  function toCanonicalToolName$2(reasonixName) {
94913
- return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
95138
+ return lookupOwn({
95139
+ record: REASONIX_TO_CANONICAL_TOOL_NAMES,
95140
+ key: reasonixName
95141
+ }) ?? reasonixName;
94914
95142
  }
94915
95143
  /**
94916
95144
  * Whether an entry uses the first-class `Tool=<literal>` exact-command form
@@ -95533,8 +95761,12 @@ function convertReasonixToRulesyncPermissions(params) {
95533
95761
  for (const entry of entries) {
95534
95762
  const { toolName, pattern } = parseToolPatternEntry(entry);
95535
95763
  const canonical = toCanonicalToolName$2(toolName);
95536
- if (!permission[canonical]) permission[canonical] = {};
95537
- permission[canonical][pattern] = action;
95764
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
95765
+ getOrCreateOwn({
95766
+ record: permission,
95767
+ key: canonical,
95768
+ create: () => ({})
95769
+ })[pattern] = action;
95538
95770
  }
95539
95771
  };
95540
95772
  processEntries(params.allow, "allow");
@@ -96086,7 +96318,10 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
96086
96318
  if (bash) toolPermissions.bash = bash;
96087
96319
  continue;
96088
96320
  }
96089
- const toolKeys = CATEGORY_TO_TOOL_KEYS[category];
96321
+ const toolKeys = lookupOwn({
96322
+ record: CATEGORY_TO_TOOL_KEYS,
96323
+ key: category
96324
+ });
96090
96325
  if (!toolKeys) {
96091
96326
  logger?.warn(`Rovo Dev permissions have no target for the "${category}" category. Skipping it.`);
96092
96327
  markSharedToolKeyRestriction({
@@ -96234,7 +96469,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
96234
96469
  }
96235
96470
  if (isStringArray(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
96236
96471
  permission.read ??= {};
96237
- permission.read[path] ??= "allow";
96472
+ if (!Object.hasOwn(permission.read, path)) permission.read[path] = "allow";
96238
96473
  }
96239
96474
  return { permission };
96240
96475
  }
@@ -97971,11 +98206,17 @@ var TraePermissions = class TraePermissions extends ToolPermissions {
97971
98206
  const permission = {};
97972
98207
  const put = (category, pattern, action) => {
97973
98208
  if (isPrototypePollutionKey(category) || isPrototypePollutionKey(pattern)) return;
97974
- const rules = permission[category] ??= {};
97975
- const existing = rules[pattern];
98209
+ const rules = getOrCreateOwn({
98210
+ record: permission,
98211
+ key: category,
98212
+ create: () => ({})
98213
+ });
97976
98214
  if (isStricterAction({
97977
98215
  action,
97978
- existing
98216
+ existing: lookupOwn({
98217
+ record: rules,
98218
+ key: pattern
98219
+ })
97979
98220
  })) rules[pattern] = action;
97980
98221
  };
97981
98222
  for (const [key, value] of Object.entries(readRecord(customProfiles, COMMAND_RULES_PATH))) {
@@ -100318,7 +100559,10 @@ function toZedToolName(canonical) {
100318
100559
  const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
100319
100560
  return `${MCP_ZED_PREFIX}${address}`;
100320
100561
  }
100321
- return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
100562
+ return lookupOwn({
100563
+ record: CANONICAL_TO_ZED_TOOL_NAMES,
100564
+ key: canonical
100565
+ }) ?? canonical;
100322
100566
  }
100323
100567
  function toCanonicalToolName(zedName) {
100324
100568
  if (zedName.startsWith(MCP_ZED_PREFIX)) {
@@ -100326,7 +100570,10 @@ function toCanonicalToolName(zedName) {
100326
100570
  const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
100327
100571
  return `${MCP_CATEGORY_PREFIX}${address}`;
100328
100572
  }
100329
- return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
100573
+ return lookupOwn({
100574
+ record: ZED_TO_CANONICAL_TOOL_NAMES,
100575
+ key: zedName
100576
+ }) ?? zedName;
100330
100577
  }
100331
100578
  /**
100332
100579
  * Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
@@ -100638,7 +100885,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
100638
100885
  const globalDefault = parsed.success ? parsed.data.default : void 0;
100639
100886
  const permission = {};
100640
100887
  const ensure = (category) => {
100641
- const existing = permission[category];
100888
+ const existing = lookupOwn({
100889
+ record: permission,
100890
+ key: category
100891
+ });
100642
100892
  if (existing) return existing;
100643
100893
  const created = {};
100644
100894
  permission[category] = created;
@@ -101265,7 +101515,7 @@ function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
101265
101515
  async function convertFromTool(params) {
101266
101516
  resetRunWarningState();
101267
101517
  const packagingTarget = [params.fromTool, ...params.toTools].find(isPackagingToolTarget);
101268
- if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import and generate with explicit plugin directories.`);
101518
+ if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import with the plugin directory as outputRoot (--output-root) and generate with it in outputRoots (--output-roots).`);
101269
101519
  const ctx = params;
101270
101520
  const [rulesCount, ignoreCount, mcpCount, commandsCount, subagentsCount, skillsCount, hooksCount, permissionsCount, checksCount] = [
101271
101521
  await runFeatureConvert(ctx, buildRulesStrategy(ctx)),
@@ -103745,6 +103995,6 @@ const CHECKS_IMPORT_STRATEGY = {
103745
103995
  missingLabel: "check files",
103746
103996
  createdLabel: "check files"
103747
103997
  };
103748
- export { RulesyncPermissions as $, RULESYNC_USER_CONFIG_DIR_NAME as $n, isRootDirPath as $t, CODEBUDDY_DIR as A, CURATED_RULES_FEATURE_SUBDIR as An, withFallbackLoggerTarget as At, CLAUDECODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Bn, assertTreeContainsNoSymlinks as Bt, QWENCODE_LOCAL_RULE_FILE_NAME as C, ALL_TOOL_TARGETS as Cn, getSourceTransport as Ct, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as D, ALL_FEATURES as Dn, WarningCollectingLogger as Dt, FACTORYDROID_DIR as E, ToolTargetSchema as En, JsonLogger as Et, groupSpellingsByCaseFoldedIdentity as F, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Fn, PathOutsideRootError as Ft, toInstructionBudgetFile as G, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Gn, ensureDir as Gt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as H, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Hn, checkPathTraversal as Ht, CODEXCLI_BASH_RULES_FILE_NAME as I, RULESYNC_CONFIG_RELATIVE_FILE_PATH as In, applyFileMode as It, RulesyncSubagentFrontmatterSchema as J, RULESYNC_RELATIVE_DIR_PATH as Jn, findFilesByGlobs as Jt, getLocalSkillDirNames as K, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Kn, fileExists as Kt, CODEXCLI_DIR as L, RULESYNC_CONFIG_SCHEMA_URL as Ln, assertDirectoryIfExists as Lt, AUGMENTCODE_DIR as M, MAX_FILE_SIZE as Mn, withWarnOnceScope as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Nn, CLIError as Nt, COMMANDCODE_DIR as O, ALL_FEATURES_WITH_WILDCARD as On, fallbackLogger as Ot, caseFoldIdentity as P, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Pn, ErrorCodes as Pt, RulesyncRuleFrontmatterSchema as Q, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Qn, isFileSystemError as Qt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as R, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Rn, assertNoSymlinkBelowRoot as Rt, QWENCODE_DIR as S, mapWithConcurrency as Sn, assertTargetsFeaturesExclusive as St, LETTACODE_SETTINGS_LOCAL_FILE_NAME as T, PACKAGING_TOOL_TARGETS as Tn, ConsoleLogger as Tt, CLAUDECODE_SKILLS_DIR_PATH as U, RULESYNC_MCP_RELATIVE_FILE_PATH as Un, createTempDirectory as Ut, CLAUDECODE_MEMORIES_DIR_NAME as V, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Vn, assertWritablePathInsideRoot as Vt, findProjectInstructionBudgetFindings as W, RULESYNC_MCP_SCHEMA_URL as Wn, directoryExists as Wt, RulesyncSkillFrontmatterSchema as X, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Xn, getHomeDirectory as Xt, RulesyncSkill as Y, RULESYNC_RULES_RELATIVE_DIR_PATH as Yn, getFileSizeAndExecutableBit as Yt, RulesyncRule as Z, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Zn, isFileNotFoundError as Zt, SubagentsProcessor as _, runWithDirectoryRollback as _n, resolveEffectiveInputRoots as _t, convertFromTool as a, pathEscapesRoot as an, hasEnclosingMarkOutsideKeycap as ar, parseJsonc as at, CommandsProcessor as b, writeFileBuffer as bn, GITIGNORE_DESTINATION_KEY as bt, parseCommaSeparatedList as c, readFileContentOrNull as cn, stripControlCharactersKeepingLineFeeds as cr, RulesyncCheck as ct, shortenToWidth as d, removeFile as dn, loadYaml as dt, isSymlink as en, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as er, RulesyncMcp as et, McpProcessor as f, removeFileStrict as fn, isRecord as ft, RulesProcessor as g, resolvedRelativePath as gn, mergeInputRootConfigs as gt, ChecksProcessor as h, resolvePath as hn, ConfigResolver as ht, inspectInputRoots as i, normalizePosixDirPath as in, hasDeceptiveHiddenCharacters as ir, resolveRulesyncSourceWritePath as it, CODEBUDDY_LOCAL_RULE_FILE_NAME as j, FEATURE_SOURCE_TREE_ENTRIES as jn, resetRunWarningState as jt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as k, DEPRECATED_FEATURE_REPLACEMENTS as kn, warnOnConflictingFlags as kt, ELLIPSIS_WIDTH as l, removeDirectory as ln, stripControlCharactersKeepingLineFeedsAndTabs as lr, RulesyncCheckFrontmatterSchema as lt, HooksProcessor as m, removeTempDirectory as mn, SKILL_FILE_NAME as mt, formatSourceLoadFailure as n, listFilePathsRecursively as nn, formatError as nr, RulesyncHooks as nt, isPackagingToolTarget as o, posixRelativePathEscapesRoot as on, quoteForLog as or, RulesyncCommand as ot, IgnoreProcessor as p, removeFileWithoutFollowingSymlinks as pn, SHARED_USER_MANAGED_CONFIG_PATHS as pt, RulesyncSubagent as q, RULESYNC_PERMISSIONS_SCHEMA_URL as qn, fileExistsStrict as qt, generate as r, listSubdirectoryNames as rn, truncateText as rr, getRulesyncSourceCandidates as rt, getProcessorRegistryEntry as s, readFileContent as sn, stripControlCharacters as sr, RulesyncCommandFrontmatterSchema as st, importFromTool as t, listDirectoryEntryNames as tn, formatMegabytes as tr, RulesyncIgnore as tt, displayWidthOf as u, removeDirectoryStrict as un, stripHiddenCharacters as ur, stringifyFrontmatter as ut, SkillsProcessor as v, splitPathSegments as vn, CONFLICTING_TARGET_PAIRS as vt, LETTACODE_DIR as w, ALL_TOOL_TARGETS_WITH_WILDCARD as wn, findControlCharacter as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, writeFileContent as xn, SourceEntrySchema as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, toPosixPath as yn, ConfigFileSchema as yt, CLAUDECODE_DIR as z, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as zn, assertPathResolvesInsideRoot as zt };
103998
+ export { RulesyncMcp as $, RULESYNC_USER_CONFIG_DIR_NAME as $n, isRootDirPath as $t, CODEBUDDY_LOCAL_RULE_FILE_NAME as A, CURATED_RULES_FEATURE_SUBDIR as An, resetRunWarningState as At, CLAUDECODE_MEMORIES_DIR_NAME as B, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Bn, assertTreeContainsNoSymlinks as Bt, LETTACODE_DIR as C, ALL_TOOL_TARGETS as Cn, findControlCharacter as Ct, COMMANDCODE_DIR as D, ALL_FEATURES as Dn, fallbackLogger as Dt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as E, ToolTargetSchema as En, WarningCollectingLogger as Et, CODEXCLI_BASH_RULES_FILE_NAME as F, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Fn, PathOutsideRootError as Ft, getLocalSkillDirNames as G, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Gn, ensureDir as Gt, CLAUDECODE_SKILLS_DIR_PATH as H, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Hn, checkPathTraversal as Ht, CODEXCLI_DIR as I, RULESYNC_CONFIG_RELATIVE_FILE_PATH as In, applyFileMode as It, RulesyncSkill as J, RULESYNC_RELATIVE_DIR_PATH as Jn, findFilesByGlobs as Jt, RulesyncSubagent as K, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Kn, fileExists as Kt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as L, RULESYNC_CONFIG_SCHEMA_URL as Ln, assertDirectoryIfExists as Lt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as M, MAX_FILE_SIZE as Mn, CLIError as Mt, caseFoldIdentity as N, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Nn, ErrorCodes as Nt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES_WITH_WILDCARD as On, warnOnConflictingFlags as Ot, groupSpellingsByCaseFoldedIdentity as P, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Pn, parseJsonc as Pt, RulesyncPermissions as Q, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Qn, isFileSystemError as Qt, CLAUDECODE_DIR as R, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Rn, assertNoSymlinkBelowRoot as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, mapWithConcurrency as Sn, getSourceTransport as St, FACTORYDROID_DIR as T, PACKAGING_TOOL_TARGETS as Tn, JsonLogger as Tt, findProjectInstructionBudgetFindings as U, RULESYNC_MCP_RELATIVE_FILE_PATH as Un, createTempDirectory as Ut, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as V, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Vn, assertWritablePathInsideRoot as Vt, toInstructionBudgetFile as W, RULESYNC_MCP_SCHEMA_URL as Wn, directoryExists as Wt, RulesyncRule as X, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Xn, getHomeDirectory as Xt, RulesyncSkillFrontmatterSchema as Y, RULESYNC_RULES_RELATIVE_DIR_PATH as Yn, getFileSizeAndExecutableBit as Yt, RulesyncRuleFrontmatterSchema as Z, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Zn, isFileNotFoundError as Zt, SkillsProcessor as _, runWithDirectoryRollback as _n, CONFLICTING_TARGET_PAIRS as _t, convertFromTool as a, pathEscapesRoot as an, hasEnclosingMarkOutsideKeycap as ar, RulesyncCommandFrontmatterSchema as at, REASONIX_LOCAL_RULE_FILE_NAME as b, writeFileBuffer as bn, SourceEntrySchema as bt, ELLIPSIS_WIDTH as c, readFileContentOrNull as cn, stripControlCharactersKeepingLineFeeds as cr, stringifyFrontmatter as ct, McpProcessor as d, removeFile as dn, SHARED_USER_MANAGED_CONFIG_PATHS as dt, isSymlink as en, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as er, RulesyncIgnore as et, IgnoreProcessor as f, removeFileStrict as fn, SKILL_FILE_NAME as ft, SubagentsProcessor as g, resolvedRelativePath as gn, resolveEffectiveInputRoots as gt, RulesProcessor as h, resolvePath as hn, parseConfigFileContent as ht, inspectInputRoots as i, normalizePosixDirPath as in, hasDeceptiveHiddenCharacters as ir, RulesyncCommand as it, AUGMENTCODE_DIR as j, FEATURE_SOURCE_TREE_ENTRIES as jn, withWarnOnceScope as jt, CODEBUDDY_DIR as k, DEPRECATED_FEATURE_REPLACEMENTS as kn, withFallbackLoggerTarget as kt, displayWidthOf as l, removeDirectory as ln, stripControlCharactersKeepingLineFeedsAndTabs as lr, loadYaml as lt, ChecksProcessor as m, removeTempDirectory as mn, mergeInputRootConfigs as mt, formatSourceLoadFailure as n, listFilePathsRecursively as nn, formatError as nr, getRulesyncSourceCandidates as nt, getProcessorRegistryEntry as o, posixRelativePathEscapesRoot as on, quoteForLog as or, RulesyncCheck as ot, HooksProcessor as p, removeFileWithoutFollowingSymlinks as pn, ConfigResolver as pt, RulesyncSubagentFrontmatterSchema as q, RULESYNC_PERMISSIONS_SCHEMA_URL as qn, fileExistsStrict as qt, generate as r, listSubdirectoryNames as rn, truncateText as rr, resolveRulesyncSourceWritePath as rt, parseCommaSeparatedList as s, readFileContent as sn, stripControlCharacters as sr, RulesyncCheckFrontmatterSchema as st, importFromTool as t, listDirectoryEntryNames as tn, formatMegabytes as tr, RulesyncHooks as tt, shortenToWidth as u, removeDirectoryStrict as un, stripHiddenCharacters as ur, isRecord as ut, CRUSH_LOCAL_RULE_FILE_NAME as v, splitPathSegments as vn, ConfigFileSchema as vt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as w, ALL_TOOL_TARGETS_WITH_WILDCARD as wn, ConsoleLogger as wt, QWENCODE_DIR as x, writeFileContent as xn, assertTargetsFeaturesExclusive as xt, CommandsProcessor as y, toPosixPath as yn, GITIGNORE_DESTINATION_KEY as yt, CLAUDECODE_LOCAL_RULE_FILE_NAME as z, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as zn, assertPathResolvesInsideRoot as zt };
103749
103999
 
103750
- //# sourceMappingURL=import-Safgeb1C.js.map
104000
+ //# sourceMappingURL=import-iLVVcze8.js.map