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.
@@ -2291,6 +2291,190 @@ async function removeTempDirectory(tempDir) {
2291
2291
  });
2292
2292
  } catch {}
2293
2293
  }
2294
+ // src/utils/prototype-pollution.ts
2295
+ /**
2296
+ * Keys that, if walked into when constructing or merging objects from
2297
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
2298
+ * chain) and propagate state to every other object in the runtime. Any code
2299
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
2300
+ * parsing, MCP config conversion, settings round-trip — should skip these.
2301
+ */
2302
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
2303
+ "__proto__",
2304
+ "constructor",
2305
+ "prototype"
2306
+ ]);
2307
+ function isPrototypePollutionKey(key) {
2308
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
2309
+ }
2310
+ /**
2311
+ * Returns a shallow copy of a record's own entries with every
2312
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
2313
+ *
2314
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
2315
+ * or `headers` table — into freshly generated config. Carrying such a map by
2316
+ * reference, or re-assigning its keys via bracket notation, would let a literal
2317
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
2318
+ * prototype). Walking the entries through this helper severs that path while
2319
+ * preserving every legitimate key.
2320
+ */
2321
+ function omitPrototypePollutionKeys(record) {
2322
+ const sanitized = {};
2323
+ for (const [key, value] of Object.entries(record)) {
2324
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2325
+ sanitized[key] = value;
2326
+ }
2327
+ return sanitized;
2328
+ }
2329
+ /**
2330
+ * Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
2331
+ * reachable through the value (directly, or through an array) is copied with
2332
+ * its prototype-pollution keys dropped. Scalars and arrays of scalars come back
2333
+ * as they are.
2334
+ *
2335
+ * Use when passing an unknown, user-supplied value through unchanged — an MCP
2336
+ * server field a tool documents that rulesync does not model — so a
2337
+ * `__proto__` key nested anywhere inside it cannot ride into the generated
2338
+ * config, where the consuming tool may merge the object without the same care.
2339
+ */
2340
+ function omitPrototypePollutionKeysDeep(value) {
2341
+ if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
2342
+ if (value === null || typeof value !== "object") return value;
2343
+ const sanitized = {};
2344
+ for (const [key, entry] of Object.entries(value)) {
2345
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2346
+ sanitized[key] = omitPrototypePollutionKeysDeep(entry);
2347
+ }
2348
+ return sanitized;
2349
+ }
2350
+ // src/utils/jsonc.ts
2351
+ /**
2352
+ * Rebuild the parsed value from its own enumerable entries, dropping
2353
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
2354
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
2355
+ * literal `__proto__` key would replace the containing object's prototype
2356
+ * instead of becoming an own property; rebuilding gives every object a clean
2357
+ * `Object.prototype` again and severs that path.
2358
+ */
2359
+ function deepSanitize(value) {
2360
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
2361
+ if (value !== null && typeof value === "object") {
2362
+ const sanitized = {};
2363
+ for (const [key, entry] of Object.entries(value)) {
2364
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2365
+ sanitized[key] = deepSanitize(entry);
2366
+ }
2367
+ return sanitized;
2368
+ }
2369
+ return value;
2370
+ }
2371
+ /**
2372
+ * Collect the paths of every prototype-pollution key in the document, walking
2373
+ * the syntax tree rather than the parsed value.
2374
+ *
2375
+ * The parsed value cannot answer this question for `__proto__`:
2376
+ * `obj["__proto__"] = "deny"` is discarded by the engine outright, and
2377
+ * `obj["__proto__"] = {...}` replaces the prototype instead of adding an own
2378
+ * property — either way the key is already gone by the time
2379
+ * {@link deepSanitize} could look for it, and only the source text still knows
2380
+ * the user wrote it. `constructor` and `prototype` do become own properties and
2381
+ * could be found in the parsed value, but they are collected here too so all
2382
+ * three keys are answered the same way.
2383
+ *
2384
+ * A pollution key's subtree is not descended into, matching
2385
+ * {@link deepSanitize}, which drops the whole value along with the key.
2386
+ */
2387
+ function collectPollutionKeyPaths({ node, path, found }) {
2388
+ if (node.type === "array") {
2389
+ for (const [index, child] of (node.children ?? []).entries()) collectPollutionKeyPaths({
2390
+ node: child,
2391
+ path: `${path}[${index}]`,
2392
+ found
2393
+ });
2394
+ return;
2395
+ }
2396
+ if (node.type !== "object") return;
2397
+ for (const property of node.children ?? []) {
2398
+ const [keyNode, valueNode] = property.children ?? [];
2399
+ if (typeof keyNode?.value !== "string") continue;
2400
+ const keyPath = path === "" ? keyNode.value : `${path}.${keyNode.value}`;
2401
+ if (PROTOTYPE_POLLUTION_KEYS.has(keyNode.value)) {
2402
+ found.push(keyPath);
2403
+ continue;
2404
+ }
2405
+ if (valueNode !== void 0) collectPollutionKeyPaths({
2406
+ node: valueNode,
2407
+ path: keyPath,
2408
+ found
2409
+ });
2410
+ }
2411
+ }
2412
+ /**
2413
+ * Parse a JSONC (JSON with Comments) document strictly.
2414
+ *
2415
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
2416
+ * errors and returns a best-effort value), this throws on any parse error so
2417
+ * a malformed source file fails loudly instead of generating half-empty tool
2418
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
2419
+ * `JSON.parse` on files that may contain comments or trailing commas.
2420
+ */
2421
+ function parseJsonc$6(content) {
2422
+ return deepSanitize(parseStrict(content));
2423
+ }
2424
+ /**
2425
+ * The strict parse shared by both entry points. Kept separate from the syntax
2426
+ * tree walk so the many callers that never look at `droppedKeys` do not pay
2427
+ * for building a second representation of the document.
2428
+ */
2429
+ function parseStrict(content) {
2430
+ const errors = [];
2431
+ const result = (0, jsonc_parser.parse)(content, errors, {
2432
+ allowTrailingComma: true,
2433
+ disallowComments: false
2434
+ });
2435
+ if (errors.length > 0) {
2436
+ const details = errors.map((e) => `${(0, jsonc_parser.printParseErrorCode)(e.error)} at offset ${e.offset}`).join(", ");
2437
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
2438
+ }
2439
+ return result;
2440
+ }
2441
+ /**
2442
+ * The error a source document should fail with when
2443
+ * {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
2444
+ *
2445
+ * Shared by every source that reports them, so the three files a user can
2446
+ * author explain the same removal the same way rather than each inventing its
2447
+ * own wording.
2448
+ */
2449
+ function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
2450
+ 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.`);
2451
+ }
2452
+ /**
2453
+ * The same parse as {@link parseJsonc}, additionally reporting which
2454
+ * prototype-pollution keys were removed, as dotted paths
2455
+ * (`permission.bash.__proto__`).
2456
+ *
2457
+ * The parse itself is identical — the paths only let a caller tell the user
2458
+ * about entries that vanished, which they would otherwise have to notice by
2459
+ * comparing their source file against the generated output.
2460
+ */
2461
+ function parseJsoncReportingDroppedKeys({ content }) {
2462
+ const result = parseStrict(content);
2463
+ const found = [];
2464
+ const tree = (0, jsonc_parser.parseTree)(content, [], {
2465
+ allowTrailingComma: true,
2466
+ disallowComments: false
2467
+ });
2468
+ if (tree !== void 0) collectPollutionKeyPaths({
2469
+ node: tree,
2470
+ path: "",
2471
+ found
2472
+ });
2473
+ return {
2474
+ value: deepSanitize(result),
2475
+ droppedKeys: [...new Set(found)]
2476
+ };
2477
+ }
2294
2478
  // src/types/json-output.ts
2295
2479
  /**
2296
2480
  * Error codes for CLI errors
@@ -3580,10 +3764,37 @@ const getDefaults = () => ({
3580
3764
  inputRoots: void 0,
3581
3765
  sources: []
3582
3766
  });
3767
+ /**
3768
+ * Parse the content of a config file (`rulesync.jsonc` / `rulesync.local.jsonc`)
3769
+ * through the same strict, sanitizing JSONC parser as every other rulesync
3770
+ * source.
3771
+ *
3772
+ * `jsonc-parser`'s bare `parse` recovers from syntax errors without reporting
3773
+ * them, which let a typo (a missing comma, unquoted keys, trailing garbage)
3774
+ * silently drop keys and run rulesync with a different config than the user
3775
+ * wrote. Prototype-pollution keys are rejected rather than silently removed,
3776
+ * matching the hooks, MCP and permissions sources.
3777
+ */
3778
+ function parseConfigFileContent({ content, filePath }) {
3779
+ let result;
3780
+ try {
3781
+ result = parseJsoncReportingDroppedKeys({ content });
3782
+ } catch (error) {
3783
+ const detail = error instanceof Error ? error.message : String(error);
3784
+ throw new Error(`Failed to parse ${JSON.stringify(filePath)}: ${detail}`, { cause: error });
3785
+ }
3786
+ if (result.droppedKeys.length > 0) throw droppedPollutionKeysError({
3787
+ sourcePath: filePath,
3788
+ droppedKeys: result.droppedKeys
3789
+ });
3790
+ return result.value;
3791
+ }
3583
3792
  const loadConfigFromFile = async (filePath) => {
3584
3793
  if (!await fileExists(filePath)) return {};
3585
- const fileContent = await readFileContent(filePath);
3586
- const jsonData = (0, jsonc_parser.parse)(fileContent);
3794
+ const jsonData = parseConfigFileContent({
3795
+ content: await readFileContent(filePath),
3796
+ filePath
3797
+ });
3587
3798
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
3588
3799
  assertTargetsFeaturesExclusive({
3589
3800
  targets: configParams.targets,
@@ -4196,62 +4407,6 @@ function createBoundedWalk({ subject, limits, root }) {
4196
4407
  leave
4197
4408
  };
4198
4409
  }
4199
- // src/utils/prototype-pollution.ts
4200
- /**
4201
- * Keys that, if walked into when constructing or merging objects from
4202
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
4203
- * chain) and propagate state to every other object in the runtime. Any code
4204
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
4205
- * parsing, MCP config conversion, settings round-trip — should skip these.
4206
- */
4207
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
4208
- "__proto__",
4209
- "constructor",
4210
- "prototype"
4211
- ]);
4212
- function isPrototypePollutionKey(key) {
4213
- return PROTOTYPE_POLLUTION_KEYS.has(key);
4214
- }
4215
- /**
4216
- * Returns a shallow copy of a record's own entries with every
4217
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
4218
- *
4219
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
4220
- * or `headers` table — into freshly generated config. Carrying such a map by
4221
- * reference, or re-assigning its keys via bracket notation, would let a literal
4222
- * `__proto__` key ride along (and re-assigning it would mutate the target's
4223
- * prototype). Walking the entries through this helper severs that path while
4224
- * preserving every legitimate key.
4225
- */
4226
- function omitPrototypePollutionKeys(record) {
4227
- const sanitized = {};
4228
- for (const [key, value] of Object.entries(record)) {
4229
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4230
- sanitized[key] = value;
4231
- }
4232
- return sanitized;
4233
- }
4234
- /**
4235
- * Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
4236
- * reachable through the value (directly, or through an array) is copied with
4237
- * its prototype-pollution keys dropped. Scalars and arrays of scalars come back
4238
- * as they are.
4239
- *
4240
- * Use when passing an unknown, user-supplied value through unchanged — an MCP
4241
- * server field a tool documents that rulesync does not model — so a
4242
- * `__proto__` key nested anywhere inside it cannot ride into the generated
4243
- * config, where the consuming tool may merge the object without the same care.
4244
- */
4245
- function omitPrototypePollutionKeysDeep(value) {
4246
- if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
4247
- if (value === null || typeof value !== "object") return value;
4248
- const sanitized = {};
4249
- for (const [key, entry] of Object.entries(value)) {
4250
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4251
- sanitized[key] = omitPrototypePollutionKeysDeep(entry);
4252
- }
4253
- return sanitized;
4254
- }
4255
4410
  // src/utils/type-guards.ts
4256
4411
  /**
4257
4412
  * Type guard to check if a value is a plain object (Record<string, unknown>).
@@ -4879,6 +5034,24 @@ function lookupOwn({ record, key }) {
4879
5034
  return Object.hasOwn(record, key) ? record[key] : void 0;
4880
5035
  }
4881
5036
  /**
5037
+ * Return the value a map holds under its own `key`, first storing `create()`
5038
+ * there when it has none. A bracket read would find an inherited member for a
5039
+ * key such as `toString`, so an importer bucketing rules by a user-supplied
5040
+ * name would write into an `Object.prototype` function instead of a fresh
5041
+ * bucket.
5042
+ */
5043
+ function getOrCreateOwn({ record, key, create }) {
5044
+ if (Object.hasOwn(record, key)) return record[key];
5045
+ const created = create();
5046
+ Object.defineProperty(record, key, {
5047
+ value: created,
5048
+ writable: true,
5049
+ enumerable: true,
5050
+ configurable: true
5051
+ });
5052
+ return created;
5053
+ }
5054
+ /**
4882
5055
  * Swap the keys and values of a name map, e.g. a canonical-to-tool map into
4883
5056
  * the tool-to-canonical map an importer reads. When two keys share a value,
4884
5057
  * the later key wins.
@@ -6413,14 +6586,16 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
6413
6586
  * Hook events supported by Goose.
6414
6587
  *
6415
6588
  * Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
6416
- * maps PascalCase event names to matcher/handler arrays. Every Goose event has a
6417
- * 1:1 canonical equivalent, so no new canonical events are required.
6418
- *
6419
- * Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
6420
- * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
6421
- * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
6589
+ * maps PascalCase event names to matcher/handler arrays.
6590
+ *
6591
+ * Goose's `HookEvent` enum defines these 11 events plus `PreToolUseResult`
6592
+ * (added in v1.48.0), which has no canonical equivalent: it is authored under
6593
+ * the `goose.hooks` override block by its native name, emitted verbatim, and
6594
+ * imported back into that block. Notably the enum has NO
6595
+ * `SubagentStart`/`SubagentStop` arms — emitting them would write keys Goose
6596
+ * silently ignores, so `subagentStart`/`subagentStop` are intentionally
6422
6597
  * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
6423
- * @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
6598
+ * @see https://github.com/aaif-goose/goose/blob/v1.48.0/crates/goose/src/hooks/mod.rs
6424
6599
  * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
6425
6600
  */
6426
6601
  const GOOSE_HOOK_EVENTS = eventsOf(CANONICAL_TO_GOOSE_EVENT_NAMES);
@@ -6898,134 +7073,6 @@ const GROKCLI_HOOK_EVENTS = eventsOf(CANONICAL_TO_GROKCLI_EVENT_NAMES);
6898
7073
  * Map Grok CLI PascalCase event names to canonical camelCase.
6899
7074
  */
6900
7075
  const GROKCLI_TO_CANONICAL_EVENT_NAMES = invertRecord(CANONICAL_TO_GROKCLI_EVENT_NAMES);
6901
- // src/utils/jsonc.ts
6902
- /**
6903
- * Rebuild the parsed value from its own enumerable entries, dropping
6904
- * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
6905
- * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
6906
- * literal `__proto__` key would replace the containing object's prototype
6907
- * instead of becoming an own property; rebuilding gives every object a clean
6908
- * `Object.prototype` again and severs that path.
6909
- */
6910
- function deepSanitize(value) {
6911
- if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
6912
- if (value !== null && typeof value === "object") {
6913
- const sanitized = {};
6914
- for (const [key, entry] of Object.entries(value)) {
6915
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6916
- sanitized[key] = deepSanitize(entry);
6917
- }
6918
- return sanitized;
6919
- }
6920
- return value;
6921
- }
6922
- /**
6923
- * Collect the paths of every prototype-pollution key in the document, walking
6924
- * the syntax tree rather than the parsed value.
6925
- *
6926
- * The parsed value cannot answer this question for `__proto__`:
6927
- * `obj["__proto__"] = "deny"` is discarded by the engine outright, and
6928
- * `obj["__proto__"] = {...}` replaces the prototype instead of adding an own
6929
- * property — either way the key is already gone by the time
6930
- * {@link deepSanitize} could look for it, and only the source text still knows
6931
- * the user wrote it. `constructor` and `prototype` do become own properties and
6932
- * could be found in the parsed value, but they are collected here too so all
6933
- * three keys are answered the same way.
6934
- *
6935
- * A pollution key's subtree is not descended into, matching
6936
- * {@link deepSanitize}, which drops the whole value along with the key.
6937
- */
6938
- function collectPollutionKeyPaths({ node, path, found }) {
6939
- if (node.type === "array") {
6940
- for (const [index, child] of (node.children ?? []).entries()) collectPollutionKeyPaths({
6941
- node: child,
6942
- path: `${path}[${index}]`,
6943
- found
6944
- });
6945
- return;
6946
- }
6947
- if (node.type !== "object") return;
6948
- for (const property of node.children ?? []) {
6949
- const [keyNode, valueNode] = property.children ?? [];
6950
- if (typeof keyNode?.value !== "string") continue;
6951
- const keyPath = path === "" ? keyNode.value : `${path}.${keyNode.value}`;
6952
- if (PROTOTYPE_POLLUTION_KEYS.has(keyNode.value)) {
6953
- found.push(keyPath);
6954
- continue;
6955
- }
6956
- if (valueNode !== void 0) collectPollutionKeyPaths({
6957
- node: valueNode,
6958
- path: keyPath,
6959
- found
6960
- });
6961
- }
6962
- }
6963
- /**
6964
- * Parse a JSONC (JSON with Comments) document strictly.
6965
- *
6966
- * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
6967
- * errors and returns a best-effort value), this throws on any parse error so
6968
- * a malformed source file fails loudly instead of generating half-empty tool
6969
- * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
6970
- * `JSON.parse` on files that may contain comments or trailing commas.
6971
- */
6972
- function parseJsonc$6(content) {
6973
- return deepSanitize(parseStrict(content));
6974
- }
6975
- /**
6976
- * The strict parse shared by both entry points. Kept separate from the syntax
6977
- * tree walk so the many callers that never look at `droppedKeys` do not pay
6978
- * for building a second representation of the document.
6979
- */
6980
- function parseStrict(content) {
6981
- const errors = [];
6982
- const result = (0, jsonc_parser.parse)(content, errors, {
6983
- allowTrailingComma: true,
6984
- disallowComments: false
6985
- });
6986
- if (errors.length > 0) {
6987
- const details = errors.map((e) => `${(0, jsonc_parser.printParseErrorCode)(e.error)} at offset ${e.offset}`).join(", ");
6988
- throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
6989
- }
6990
- return result;
6991
- }
6992
- /**
6993
- * The error a source document should fail with when
6994
- * {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
6995
- *
6996
- * Shared by every source that reports them, so the three files a user can
6997
- * author explain the same removal the same way rather than each inventing its
6998
- * own wording.
6999
- */
7000
- function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
7001
- 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.`);
7002
- }
7003
- /**
7004
- * The same parse as {@link parseJsonc}, additionally reporting which
7005
- * prototype-pollution keys were removed, as dotted paths
7006
- * (`permission.bash.__proto__`).
7007
- *
7008
- * The parse itself is identical — the paths only let a caller tell the user
7009
- * about entries that vanished, which they would otherwise have to notice by
7010
- * comparing their source file against the generated output.
7011
- */
7012
- function parseJsoncReportingDroppedKeys({ content }) {
7013
- const result = parseStrict(content);
7014
- const found = [];
7015
- const tree = (0, jsonc_parser.parseTree)(content, [], {
7016
- allowTrailingComma: true,
7017
- disallowComments: false
7018
- });
7019
- if (tree !== void 0) collectPollutionKeyPaths({
7020
- node: tree,
7021
- path: "",
7022
- found
7023
- });
7024
- return {
7025
- value: deepSanitize(result),
7026
- droppedKeys: [...new Set(found)]
7027
- };
7028
- }
7029
7076
  // src/utils/rulesync-source-path.ts
7030
7077
  /**
7031
7078
  * No `.rulesync/` source file exists for a feature at any of its candidate
@@ -19580,9 +19627,6 @@ var KiloCommand = class extends FrontmatterMarkdownToolCommand {
19580
19627
  static getFrontmatterSchema() {
19581
19628
  return KiloCommandFrontmatterSchema;
19582
19629
  }
19583
- getRulesyncOutputRoot() {
19584
- return process.cwd();
19585
- }
19586
19630
  /**
19587
19631
  * Refuse to write a command Kilo reserves (see
19588
19632
  * {@link KILO_RESERVED_COMMAND_NAMES}). The name Kilo loads is the file
@@ -20871,9 +20915,6 @@ var OpenCodeCommand = class extends FrontmatterMarkdownToolCommand {
20871
20915
  ...Object.keys(restFields).length > 0 && { [toolTarget]: restFields }
20872
20916
  };
20873
20917
  }
20874
- getRulesyncOutputRoot() {
20875
- return process.cwd();
20876
- }
20877
20918
  /**
20878
20919
  * Imports commands defined inline in `opencode.json` / `opencode.jsonc` (and,
20879
20920
  * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
@@ -58630,12 +58671,11 @@ var GooseHooks = class GooseHooks extends ToolHooks {
58630
58671
  });
58631
58672
  }
58632
58673
  toRulesyncHooks({ logger } = {}) {
58633
- const parsed = this.parseFileContentOrThrow({
58634
- displayName: "Goose",
58635
- parse: (fileContent) => JSON.parse(fileContent)
58636
- });
58637
58674
  const hooks = toolHooksToCanonical({
58638
- 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,
58675
+ hooks: this.parseFileContentOrThrow({
58676
+ displayName: "Goose",
58677
+ parse: (fileContent) => JSON.parse(fileContent)
58678
+ }).hooks,
58639
58679
  converterConfig: GOOSE_CONVERTER_CONFIG,
58640
58680
  logger
58641
58681
  });
@@ -61902,12 +61942,14 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
61902
61942
  const paths = ReasonixHooks.getSettablePaths({ global });
61903
61943
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
61904
61944
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
61905
- let settings;
61945
+ let parsed;
61906
61946
  try {
61907
- settings = JSON.parse(existingContent);
61947
+ parsed = JSON.parse(existingContent);
61908
61948
  } catch (error) {
61909
61949
  throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
61910
61950
  }
61951
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: expected a mapping at the root`);
61952
+ const settings = parsed;
61911
61953
  const config = rulesyncHooks.getJson();
61912
61954
  const reasonixHooks = canonicalToReasonixHooks({
61913
61955
  config,
@@ -62866,15 +62908,18 @@ function emittedEventsWithHook(params) {
62866
62908
  }
62867
62909
  /**
62868
62910
  * The capabilities an adapter built on the shared hooks converter already
62869
- * declares in its converter config: the events it emits and the hook types it
62870
- * keeps. Reading them from there keeps the processor's skip warnings in step
62871
- * with what the converter actually writes.
62911
+ * declares in its converter config: the events it emits, the hook types it
62912
+ * keeps, and whether it writes the per-hook `enabled` flag. Reading them from
62913
+ * there keeps the processor's skip and dropped-field warnings in step with
62914
+ * what the converter actually writes.
62872
62915
  */
62873
62916
  function converterCapabilities(config) {
62874
62917
  if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
62918
+ const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
62875
62919
  return {
62876
62920
  supportedEvents: config.supportedEvents,
62877
- supportedHookTypes: [...config.supportedHookTypes]
62921
+ supportedHookTypes: [...config.supportedHookTypes],
62922
+ ...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
62878
62923
  };
62879
62924
  }
62880
62925
  const toolHooksFactories = /* @__PURE__ */ new Map([
@@ -63496,8 +63541,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
63496
63541
  supportsImport: true
63497
63542
  },
63498
63543
  ...converterCapabilities(ZCODE_CONVERTER_CONFIG),
63499
- supportsMatcher: true,
63500
- supportsPerHookEnabled: true
63544
+ supportsMatcher: true
63501
63545
  }],
63502
63546
  ["zcode-plugin", {
63503
63547
  class: ZcodePluginHooks,
@@ -63507,10 +63551,12 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
63507
63551
  supportsImport: true
63508
63552
  },
63509
63553
  ...converterCapabilities(ZCODE_PLUGIN_CONVERTER_CONFIG),
63510
- supportsMatcher: true,
63511
- supportsPerHookEnabled: true
63554
+ supportsMatcher: true
63512
63555
  }]
63513
63556
  ]);
63557
+ function targetsSupporting(capability) {
63558
+ return [...toolHooksFactories.entries()].filter(([, f]) => f[capability]).map(([t]) => t).join(" / ");
63559
+ }
63514
63560
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
63515
63561
  const hooksProcessorToolTargetsGlobal = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsGlobal).map(([t]) => t);
63516
63562
  const hooksProcessorToolTargetsImportable = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject && f.meta.supportsImport).map(([t]) => t);
@@ -63671,7 +63717,7 @@ var HooksProcessor = class extends FeatureProcessor {
63671
63717
  effectiveHooks,
63672
63718
  predicate: (def) => def.enabled === false
63673
63719
  });
63674
- 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(", ")}`);
63720
+ 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(", ")}`);
63675
63721
  }
63676
63722
  if (!factory.supportsPerHookConfirm) {
63677
63723
  const eventsWithConfirmHooks = emittedEventsWithHook({
@@ -63680,7 +63726,7 @@ var HooksProcessor = class extends FeatureProcessor {
63680
63726
  effectiveHooks,
63681
63727
  predicate: (def) => def.confirm !== void 0
63682
63728
  });
63683
- 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(", ")}`);
63729
+ 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(", ")}`);
63684
63730
  }
63685
63731
  }
63686
63732
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -64943,12 +64989,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
64943
64989
  }
64944
64990
  toRulesyncIgnore() {
64945
64991
  const rulesyncPatterns = (0, es_toolkit.uniq)(this.patterns.map((entry) => readDenyPatternOf(entry)).filter((pattern) => pattern !== null && pattern.length > 0));
64946
- return new RulesyncIgnore({
64947
- outputRoot: this.outputRoot,
64948
- relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
64949
- relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
64950
- fileContent: rulesyncPatterns.join("\n")
64951
- });
64992
+ return this.toRulesyncIgnoreDefault({ fileContent: rulesyncPatterns.join("\n") });
64952
64993
  }
64953
64994
  static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
64954
64995
  await warnIfReasonixNestedProjectConfigShadowed({
@@ -78655,11 +78696,17 @@ function convertAmpToRulesync({ disable, permissions }) {
78655
78696
  const assign = (tool, pattern, action) => {
78656
78697
  if (isPrototypePollutionKey(tool)) return;
78657
78698
  if (isPrototypePollutionKey(pattern)) return;
78658
- const bucket = permission[tool] ??= {};
78659
- const existing = bucket[pattern];
78699
+ const bucket = getOrCreateOwn({
78700
+ record: permission,
78701
+ key: tool,
78702
+ create: () => ({})
78703
+ });
78660
78704
  if (isStricterAction({
78661
78705
  action,
78662
- existing
78706
+ existing: lookupOwn({
78707
+ record: bucket,
78708
+ key: pattern
78709
+ })
78663
78710
  })) bucket[pattern] = action;
78664
78711
  };
78665
78712
  for (const tool of disable) assign(tool, "*", "deny");
@@ -80062,8 +80109,12 @@ function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory,
80062
80109
  logger,
80063
80110
  toolLabel
80064
80111
  }) : target;
80065
- if (!permission[canonical]) permission[canonical] = {};
80066
- permission[canonical][pattern] = action;
80112
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
80113
+ getOrCreateOwn({
80114
+ record: permission,
80115
+ key: canonical,
80116
+ create: () => ({})
80117
+ })[pattern] = action;
80067
80118
  }
80068
80119
  };
80069
80120
  processEntries(allow, "allow");
@@ -80131,10 +80182,16 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
80131
80182
  };
80132
80183
  function toAntigravityCliToolName(canonical) {
80133
80184
  if (canonical.startsWith("mcp__")) return "mcp";
80134
- return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
80185
+ return lookupOwn({
80186
+ record: CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES,
80187
+ key: canonical
80188
+ }) ?? canonical;
80135
80189
  }
80136
80190
  function toCanonicalToolName$9(cliName) {
80137
- return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
80191
+ return lookupOwn({
80192
+ record: ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES,
80193
+ key: cliName
80194
+ }) ?? cliName;
80138
80195
  }
80139
80196
  /**
80140
80197
  * Permissions generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
@@ -80297,10 +80354,16 @@ const IDE_ACTION_TO_CANONICAL = {
80297
80354
  };
80298
80355
  function toIdeAction(canonical) {
80299
80356
  if (canonical.startsWith("mcp__")) return "mcp";
80300
- return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
80357
+ return lookupOwn({
80358
+ record: CANONICAL_TO_IDE_ACTION,
80359
+ key: canonical
80360
+ }) ?? canonical;
80301
80361
  }
80302
80362
  function toCanonicalCategory$2(ideAction) {
80303
- return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
80363
+ return lookupOwn({
80364
+ record: IDE_ACTION_TO_CANONICAL,
80365
+ key: ideAction
80366
+ }) ?? ideAction;
80304
80367
  }
80305
80368
  /**
80306
80369
  * Permissions generator for the Google Antigravity IDE (Antigravity 2.0).
@@ -81839,10 +81902,16 @@ const CANONICAL_TO_CLAUDE_TOOL_NAMES = {
81839
81902
  */
81840
81903
  const CLAUDE_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_CLAUDE_TOOL_NAMES);
81841
81904
  function toClaudeToolName(canonical) {
81842
- return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
81905
+ return lookupOwn({
81906
+ record: CANONICAL_TO_CLAUDE_TOOL_NAMES,
81907
+ key: canonical
81908
+ }) ?? canonical;
81843
81909
  }
81844
81910
  function toCanonicalToolName$7(claudeName) {
81845
- return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
81911
+ return lookupOwn({
81912
+ record: CLAUDE_TO_CANONICAL_TOOL_NAMES,
81913
+ key: claudeName
81914
+ }) ?? claudeName;
81846
81915
  }
81847
81916
  /**
81848
81917
  * Merge `patch` into `base`, recursing into plain objects so a sibling key at
@@ -82814,7 +82883,10 @@ const CLAUDE_PATH_RULE_ALIASES = {
82814
82883
  */
82815
82884
  function buildClaudePermissionEntry(toolName, pattern) {
82816
82885
  if (pattern === "*") return toolName;
82817
- return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
82886
+ return `${lookupOwn({
82887
+ record: CLAUDE_PATH_RULE_ALIASES,
82888
+ key: toolName
82889
+ }) ?? toolName}(${pattern})`;
82818
82890
  }
82819
82891
  /**
82820
82892
  * The Claude tool names the canonical config manages. Deliberately the tool
@@ -83052,8 +83124,12 @@ function convertClaudeToRulesyncPermissions(params) {
83052
83124
  for (const entry of entries) {
83053
83125
  const { toolName, pattern } = parseToolPatternEntry(entry);
83054
83126
  const canonical = toCanonicalToolName$7(toolName);
83055
- if (!permission[canonical]) permission[canonical] = {};
83056
- permission[canonical][pattern] = action;
83127
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
83128
+ getOrCreateOwn({
83129
+ record: permission,
83130
+ key: canonical,
83131
+ create: () => ({})
83132
+ })[pattern] = action;
83057
83133
  }
83058
83134
  };
83059
83135
  processEntries(params.allow, "allow");
@@ -83635,11 +83711,17 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
83635
83711
  for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringArray$1(permissions[action])) {
83636
83712
  const parsed = parseCodebuddyRule(rule);
83637
83713
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
83638
- const rules = permission[parsed.category] ??= {};
83639
- const existing = rules[parsed.pattern];
83714
+ const rules = getOrCreateOwn({
83715
+ record: permission,
83716
+ key: parsed.category,
83717
+ create: () => ({})
83718
+ });
83640
83719
  if (isStricterAction({
83641
83720
  action,
83642
- existing
83721
+ existing: lookupOwn({
83722
+ record: rules,
83723
+ key: parsed.pattern
83724
+ })
83643
83725
  })) rules[parsed.pattern] = action;
83644
83726
  }
83645
83727
  const codebuddy = liftCodebuddyOverride({
@@ -84149,7 +84231,10 @@ const CODEWHALE_WRITE_RESTRICTION_TOOLS = ["write_file", "apply_patch"];
84149
84231
  /** The Codewhale tools a canonical path rule is written for. */
84150
84232
  function pathCategoryTools(category, action) {
84151
84233
  if (category === "write" && action !== "allow") return CODEWHALE_WRITE_RESTRICTION_TOOLS;
84152
- return CODEWHALE_PATH_CATEGORY_TOOLS[category] ?? [];
84234
+ return lookupOwn({
84235
+ record: CODEWHALE_PATH_CATEGORY_TOOLS,
84236
+ key: category
84237
+ }) ?? [];
84153
84238
  }
84154
84239
  /** The canonical path categories a Codewhale file tool's rule can come from. */
84155
84240
  const CODEWHALE_TOOL_TO_PATH_CATEGORIES = {
@@ -84468,7 +84553,10 @@ function canonicalToCodewhaleRules({ config, preservedRules, logger }) {
84468
84553
  });
84469
84554
  for (const [category, rules] of Object.entries(config.permission)) {
84470
84555
  if (category === "bash" || category === "*") continue;
84471
- if (CODEWHALE_PATH_CATEGORY_TOOLS[category] === void 0) {
84556
+ if (lookupOwn({
84557
+ record: CODEWHALE_PATH_CATEGORY_TOOLS,
84558
+ key: category
84559
+ }) === void 0) {
84472
84560
  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.`);
84473
84561
  continue;
84474
84562
  }
@@ -84578,7 +84666,10 @@ function toImportCandidates(rule) {
84578
84666
  }
84579
84667
  const pattern = pathRuleToPattern(rule);
84580
84668
  if (pattern === null) return [];
84581
- return (CODEWHALE_TOOL_TO_PATH_CATEGORIES[rule.tool] ?? []).map((category) => ({
84669
+ return (lookupOwn({
84670
+ record: CODEWHALE_TOOL_TO_PATH_CATEGORIES,
84671
+ key: rule.tool
84672
+ }) ?? []).map((category) => ({
84582
84673
  category,
84583
84674
  pattern,
84584
84675
  rule
@@ -86186,10 +86277,17 @@ function parseCommandcodeRuleLists(permissions) {
86186
86277
  rule,
86187
86278
  category
86188
86279
  })) 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.`);
86189
- const rules = permission[category] ??= {};
86280
+ const rules = getOrCreateOwn({
86281
+ record: permission,
86282
+ key: category,
86283
+ create: () => ({})
86284
+ });
86190
86285
  if (isStricterAction({
86191
86286
  action,
86192
- existing: rules[pattern]
86287
+ existing: lookupOwn({
86288
+ record: rules,
86289
+ key: pattern
86290
+ })
86193
86291
  })) rules[pattern] = action;
86194
86292
  }
86195
86293
  }
@@ -86550,9 +86648,12 @@ function convertContinuePermissionsToRulesync(lists) {
86550
86648
  const parsedEntry = parseContinuePermissionEntry(entry);
86551
86649
  if (parsedEntry === void 0) continue;
86552
86650
  if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
86553
- const category = toCanonicalToolName$6(parsedEntry.toolName);
86554
- permission[category] ??= {};
86555
- permission[category][parsedEntry.pattern] ??= action;
86651
+ const rules = getOrCreateOwn({
86652
+ record: permission,
86653
+ key: toCanonicalToolName$6(parsedEntry.toolName),
86654
+ create: () => ({})
86655
+ });
86656
+ if (!Object.hasOwn(rules, parsedEntry.pattern)) rules[parsedEntry.pattern] = action;
86556
86657
  }
86557
86658
  }
86558
86659
  return { permission };
@@ -87181,7 +87282,9 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
87181
87282
  const settings = this.parseSettings();
87182
87283
  const rules = {};
87183
87284
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
87184
- if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
87285
+ if (this.global) {
87286
+ for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) if (!Object.hasOwn(rules, pattern)) rules[pattern] = "allow";
87287
+ }
87185
87288
  const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
87186
87289
  const defaultPermissionMode = this.global ? settings[DEFAULT_PERMISSION_MODE_KEY] : void 0;
87187
87290
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -87528,7 +87631,10 @@ function isMcpScopedCategory(canonical) {
87528
87631
  }
87529
87632
  function toCursorType(canonical) {
87530
87633
  if (isMcpScopedCategory(canonical)) return "Mcp";
87531
- return CANONICAL_TO_CURSOR_TYPE[canonical] ?? canonical;
87634
+ return lookupOwn({
87635
+ record: CANONICAL_TO_CURSOR_TYPE,
87636
+ key: canonical
87637
+ }) ?? canonical;
87532
87638
  }
87533
87639
  /**
87534
87640
  * For per-tool MCP canonical categories like `mcp__puppeteer__navigate`, the
@@ -87560,9 +87666,15 @@ function toCanonicalCategory$1(cursorType, pattern) {
87560
87666
  const tool = match[2] ?? "*";
87561
87667
  return `${MCP_CATEGORY_PREFIX}${server}__${tool}`;
87562
87668
  }
87563
- return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
87669
+ return lookupOwn({
87670
+ record: CURSOR_TYPE_TO_CANONICAL,
87671
+ key: cursorType
87672
+ }) ?? cursorType.toLowerCase();
87564
87673
  }
87565
- return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
87674
+ return lookupOwn({
87675
+ record: CURSOR_TYPE_TO_CANONICAL,
87676
+ key: cursorType
87677
+ }) ?? cursorType.toLowerCase();
87566
87678
  }
87567
87679
  /**
87568
87680
  * Narrow `JSON.parse` output to a plain object before treating it as a Cursor
@@ -87807,9 +87919,13 @@ function convertCursorToRulesyncPermissions(params) {
87807
87919
  for (const entry of entries) {
87808
87920
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
87809
87921
  const canonical = toCanonicalCategory$1(type, pattern);
87810
- if (!permission[canonical]) permission[canonical] = {};
87811
87922
  const canonicalPattern = type === "Mcp" && canonical.startsWith("mcp__") ? "*" : pattern;
87812
- permission[canonical][canonicalPattern] = action;
87923
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(canonicalPattern)) continue;
87924
+ getOrCreateOwn({
87925
+ record: permission,
87926
+ key: canonical,
87927
+ create: () => ({})
87928
+ })[canonicalPattern] = action;
87813
87929
  }
87814
87930
  };
87815
87931
  processEntries(params.allow, "allow");
@@ -88736,10 +88852,16 @@ const DEVIN_SCOPE_TO_CANONICAL = {
88736
88852
  */
88737
88853
  const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
88738
88854
  function toDevinScope(canonical) {
88739
- return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
88855
+ return lookupOwn({
88856
+ record: CANONICAL_TO_DEVIN_SCOPE,
88857
+ key: canonical
88858
+ }) ?? canonical;
88740
88859
  }
88741
88860
  function toCanonicalCategory(devinScope) {
88742
- return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
88861
+ return lookupOwn({
88862
+ record: DEVIN_SCOPE_TO_CANONICAL,
88863
+ key: devinScope
88864
+ }) ?? devinScope;
88743
88865
  }
88744
88866
  function asDevinRecord(value) {
88745
88867
  return isRecord(value) ? { ...value } : {};
@@ -89091,7 +89213,11 @@ function convertDevinToRulesyncPermissions(params) {
89091
89213
  const { toolName: scope, pattern } = parseToolPatternEntry(entry);
89092
89214
  if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
89093
89215
  const canonical = toCanonicalCategory(scope);
89094
- (permission[canonical] ??= {})[pattern] = action;
89216
+ getOrCreateOwn({
89217
+ record: permission,
89218
+ key: canonical,
89219
+ create: () => ({})
89220
+ })[pattern] = action;
89095
89221
  }
89096
89222
  };
89097
89223
  processEntries(params.allow, "allow");
@@ -89397,7 +89523,10 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
89397
89523
  };
89398
89524
  const assigned = /* @__PURE__ */ new Map();
89399
89525
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
89400
- const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
89526
+ const toolName = lookupOwn({
89527
+ record: RULESYNC_TO_GOOSE_TOOL_NAME,
89528
+ key: category
89529
+ }) ?? category;
89401
89530
  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.`);
89402
89531
  const { action, downgraded } = failSafeCatchAllAction({
89403
89532
  rules,
@@ -89429,9 +89558,16 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
89429
89558
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
89430
89559
  const toolNames = isStringArray(userPermission[key]) ? userPermission[key] : [];
89431
89560
  const action = GOOSE_LIST_TO_ACTION[key];
89432
- for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
89433
- permission[category] ??= {};
89434
- permission[category]["*"] = action;
89561
+ for (const toolName of toolNames) for (const category of lookupOwn({
89562
+ record: GOOSE_TO_RULESYNC_TOOL_NAMES,
89563
+ key: toolName
89564
+ }) ?? [toolName]) {
89565
+ if (isPrototypePollutionKey(category)) continue;
89566
+ getOrCreateOwn({
89567
+ record: permission,
89568
+ key: category,
89569
+ create: () => ({})
89570
+ })["*"] = action;
89435
89571
  }
89436
89572
  }
89437
89573
  return { permission };
@@ -89512,7 +89648,10 @@ function buildGrokEntry(category, pattern) {
89512
89648
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
89513
89649
  }
89514
89650
  if (category === "mcp") return pattern === "*" || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
89515
- const tool = CATEGORY_TO_GROK_TOOL[category];
89651
+ const tool = lookupOwn({
89652
+ record: CATEGORY_TO_GROK_TOOL,
89653
+ key: category
89654
+ });
89516
89655
  if (tool === void 0) return null;
89517
89656
  return pattern === "*" || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
89518
89657
  }
@@ -89546,7 +89685,10 @@ function parseGrokEntry(entry) {
89546
89685
  category: "mcp",
89547
89686
  pattern: "*"
89548
89687
  };
89549
- const category = GROK_TOOL_TO_CATEGORY[tool];
89688
+ const category = lookupOwn({
89689
+ record: GROK_TOOL_TO_CATEGORY,
89690
+ key: tool
89691
+ });
89550
89692
  if (category === void 0) return null;
89551
89693
  return {
89552
89694
  category,
@@ -89867,11 +90009,17 @@ function parseGrokPermissionArrays(permission) {
89867
90009
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
89868
90010
  const result = {};
89869
90011
  const record = ({ category, pattern, action }) => {
89870
- const bucket = result[category] ??= {};
89871
- const existing = bucket[pattern];
90012
+ const bucket = getOrCreateOwn({
90013
+ record: result,
90014
+ key: category,
90015
+ create: () => ({})
90016
+ });
89872
90017
  if (isStricterAction({
89873
90018
  action,
89874
- existing
90019
+ existing: lookupOwn({
90020
+ record: bucket,
90021
+ key: pattern
90022
+ })
89875
90023
  })) bucket[pattern] = action;
89876
90024
  };
89877
90025
  const applyEntries = (entries, action) => {
@@ -89928,7 +90076,11 @@ function deleteRulesByAction(rules, action) {
89928
90076
  for (const [pattern, currentAction] of Object.entries(rules)) if (currentAction === action) delete rules[pattern];
89929
90077
  }
89930
90078
  function ensureCategory(permission, category) {
89931
- return permission[category] ??= {};
90079
+ return getOrCreateOwn({
90080
+ record: permission,
90081
+ key: category,
90082
+ create: () => ({})
90083
+ });
89932
90084
  }
89933
90085
  function removeEmptyCategories(permission) {
89934
90086
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
@@ -90571,7 +90723,10 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
90571
90723
  function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
90572
90724
  const ruleLists = {};
90573
90725
  for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
90574
- const group = CANONICAL_TO_JUNIE_GROUP[category];
90726
+ const group = lookupOwn({
90727
+ record: CANONICAL_TO_JUNIE_GROUP,
90728
+ key: category
90729
+ });
90575
90730
  if (!group) {
90576
90731
  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.`);
90577
90732
  continue;
@@ -90969,7 +91124,10 @@ const CATEGORY_TO_KIMI_CODE_TOOL = {
90969
91124
  function buildKimiCodePattern(category, pattern) {
90970
91125
  if (category.startsWith("mcp__")) return pattern === "*" || pattern === "" ? category : null;
90971
91126
  if (category === "mcp") return pattern === "*" || pattern === "" ? "mcp__*" : `mcp__${pattern}`;
90972
- const tool = CATEGORY_TO_KIMI_CODE_TOOL[category];
91127
+ const tool = lookupOwn({
91128
+ record: CATEGORY_TO_KIMI_CODE_TOOL,
91129
+ key: category
91130
+ });
90973
91131
  if (!tool) return null;
90974
91132
  return pattern === "*" || pattern === "" ? tool : `${tool}(${pattern})`;
90975
91133
  }
@@ -91065,7 +91223,10 @@ function canonicalToKimiCodeRules({ config, logger }) {
91065
91223
  const canonicalRules = [];
91066
91224
  const overrideRules = config["kimi-code"]?.rules ?? [];
91067
91225
  for (const [category, rules] of Object.entries(config.permission)) {
91068
- if (!(category === "mcp" || category.startsWith("mcp__") || CATEGORY_TO_KIMI_CODE_TOOL[category] !== void 0)) {
91226
+ if (!(category === "mcp" || category.startsWith("mcp__") || lookupOwn({
91227
+ record: CATEGORY_TO_KIMI_CODE_TOOL,
91228
+ key: category
91229
+ }) !== void 0)) {
91069
91230
  if (Object.keys(rules).length > 0) logger?.warn(`Kimi Code permissions: skipping unsupported category "${category}".`);
91070
91231
  continue;
91071
91232
  }
@@ -91882,9 +92043,16 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
91882
92043
  if (!isImportableRule(rule) || !isRecord(rule) || !isPermissionAction(rule.effect)) continue;
91883
92044
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
91884
92045
  const patterns = rule.match === void 0 ? ["*"] : isStringArray(rule.match) ? rule.match : [];
91885
- const categoryRules = permission[category] ??= {};
92046
+ const categoryRules = getOrCreateOwn({
92047
+ record: permission,
92048
+ key: category,
92049
+ create: () => ({})
92050
+ });
91886
92051
  for (const pattern of patterns) {
91887
- const previous = categoryRules[pattern];
92052
+ const previous = lookupOwn({
92053
+ record: categoryRules,
92054
+ key: pattern
92055
+ });
91888
92056
  if (isStricterAction({
91889
92057
  action: rule.effect,
91890
92058
  existing: previous
@@ -91902,7 +92070,10 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
91902
92070
  function buildKiroCapabilityRules({ config, logger }) {
91903
92071
  const buckets = /* @__PURE__ */ new Map();
91904
92072
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
91905
- const capability = CANONICAL_TO_KIRO_CAPABILITY[category];
92073
+ const capability = lookupOwn({
92074
+ record: CANONICAL_TO_KIRO_CAPABILITY,
92075
+ key: category
92076
+ });
91906
92077
  if (capability === void 0) {
91907
92078
  logger?.warn(`Kiro permissions.yaml has no capability for category: ${category}. Skipping.`);
91908
92079
  continue;
@@ -92062,7 +92233,10 @@ function toLettacodeBashPayload({ pattern, action, logger }) {
92062
92233
  return prefix === "" ? "" : `${prefix}${PREFIX_SUFFIX}`;
92063
92234
  }
92064
92235
  function toLettacodeRule({ category, pattern, action, logger }) {
92065
- const tool = CATEGORY_TO_LETTACODE_TOOL[category];
92236
+ const tool = lookupOwn({
92237
+ record: CATEGORY_TO_LETTACODE_TOOL,
92238
+ key: category
92239
+ });
92066
92240
  if (tool === void 0) return null;
92067
92241
  const payload = category === "bash" ? toLettacodeBashPayload({
92068
92242
  pattern,
@@ -92217,11 +92391,17 @@ var LettacodePermissions = class LettacodePermissions extends ToolPermissions {
92217
92391
  for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringArray$1(permissions[key])) {
92218
92392
  const parsed = fromLettacodeRule(rule);
92219
92393
  if (parsed === null || isPrototypePollutionKey(parsed.pattern)) continue;
92220
- const rules = permission[parsed.category] ??= {};
92221
- const existing = rules[parsed.pattern];
92394
+ const rules = getOrCreateOwn({
92395
+ record: permission,
92396
+ key: parsed.category,
92397
+ create: () => ({})
92398
+ });
92222
92399
  if (isStricterAction({
92223
92400
  action,
92224
- existing
92401
+ existing: lookupOwn({
92402
+ record: rules,
92403
+ key: parsed.pattern
92404
+ })
92225
92405
  })) rules[parsed.pattern] = action;
92226
92406
  }
92227
92407
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
@@ -92408,7 +92588,13 @@ function getOpencodeMcpServerNames(mcp) {
92408
92588
  return isRecord(lowered) ? Object.keys(lowered) : [];
92409
92589
  }
92410
92590
  function toOpencodePermissionKey(canonical) {
92411
- return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? CANONICAL_TO_OPENCODE_FOLDED_PERMISSION_KEYS[canonical] ?? canonical;
92591
+ return lookupOwn({
92592
+ record: CANONICAL_TO_OPENCODE_PERMISSION_KEYS,
92593
+ key: canonical
92594
+ }) ?? lookupOwn({
92595
+ record: CANONICAL_TO_OPENCODE_FOLDED_PERMISSION_KEYS,
92596
+ key: canonical
92597
+ }) ?? canonical;
92412
92598
  }
92413
92599
  /**
92414
92600
  * Merge two canonical pattern maps that land on the same OpenCode key.
@@ -92432,7 +92618,10 @@ function mergePatternRules(base, incoming) {
92432
92618
  return Object.fromEntries(Object.entries(merged).toSorted(([, a], [, b]) => PERMISSION_ACTION_PRIORITY[a] - PERMISSION_ACTION_PRIORITY[b]));
92433
92619
  }
92434
92620
  function toCanonicalPermissionKey(opencodeKey) {
92435
- return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
92621
+ return lookupOwn({
92622
+ record: OPENCODE_TO_CANONICAL_PERMISSION_KEYS,
92623
+ key: opencodeKey
92624
+ }) ?? opencodeKey;
92436
92625
  }
92437
92626
  function isSharedPermissionCategory(category) {
92438
92627
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES$1.has(category) || category.startsWith("mcp__");
@@ -93714,7 +93903,14 @@ function importPoolTool({ toolConfig, bucket }) {
93714
93903
  const denied = isStringArray(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
93715
93904
  const allowed = isStringArray(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
93716
93905
  for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
93717
- for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
93906
+ for (const pattern of allowed.filter(isUsablePattern)) if (!Object.hasOwn(bucket, pattern)) bucket[pattern] = "allow";
93907
+ }
93908
+ /**
93909
+ * Record `allow` for a path no deny has claimed. The check is on own
93910
+ * properties, so a path such as `toString` is not mistaken for a stored rule.
93911
+ */
93912
+ function allowUnlessSet(bucket, path) {
93913
+ if (!Object.hasOwn(bucket, path)) bucket[path] = "allow";
93718
93914
  }
93719
93915
  /**
93720
93916
  * Convert Pool's `tools` and `paths` blocks back into a rulesync config.
@@ -93749,8 +93945,8 @@ function convertPoolToRulesync({ tools, paths }) {
93749
93945
  for (const entry of allowEntries) {
93750
93946
  const path = pathOfEntry(entry);
93751
93947
  if (path === void 0) continue;
93752
- bucketFor(READ_CATEGORY)[path] ??= "allow";
93753
- if (isRecord(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
93948
+ allowUnlessSet(bucketFor(READ_CATEGORY), path);
93949
+ if (isRecord(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) allowUnlessSet(bucketFor(category), path);
93754
93950
  }
93755
93951
  return { permission };
93756
93952
  }
@@ -93879,7 +94075,10 @@ function toQoderRule({ category, pattern, logger }) {
93879
94075
  logger?.warn(`Qoder permissions: skipping the '${category}' rule '${pattern}' — Qoder matches MCP tools by name only.`);
93880
94076
  return null;
93881
94077
  }
93882
- const tool = CATEGORY_TO_QODER_TOOL[category];
94078
+ const tool = lookupOwn({
94079
+ record: CATEGORY_TO_QODER_TOOL,
94080
+ key: category
94081
+ });
93883
94082
  if (tool === void 0) return null;
93884
94083
  if (pattern === "*") return tool;
93885
94084
  return `${QODER_PATH_RULE_ALIASES[tool] ?? tool}(${pattern})`;
@@ -94165,11 +94364,17 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
94165
94364
  for (const action of ACTIONS) for (const rule of toStringArray$1(permissions[action])) {
94166
94365
  const parsed = fromQoderRule(rule);
94167
94366
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
94168
- const rules = permission[parsed.category] ??= {};
94169
- const existing = rules[parsed.pattern];
94367
+ const rules = getOrCreateOwn({
94368
+ record: permission,
94369
+ key: parsed.category,
94370
+ create: () => ({})
94371
+ });
94170
94372
  if (isStricterAction({
94171
94373
  action,
94172
- existing
94374
+ existing: lookupOwn({
94375
+ record: rules,
94376
+ key: parsed.pattern
94377
+ })
94173
94378
  })) rules[parsed.pattern] = action;
94174
94379
  }
94175
94380
  const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
@@ -94233,10 +94438,16 @@ const CANONICAL_TO_QWEN_TOOL_NAMES = {
94233
94438
  };
94234
94439
  const QWEN_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_QWEN_TOOL_NAMES);
94235
94440
  function toQwenToolName(canonical) {
94236
- return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
94441
+ return lookupOwn({
94442
+ record: CANONICAL_TO_QWEN_TOOL_NAMES,
94443
+ key: canonical
94444
+ }) ?? canonical;
94237
94445
  }
94238
94446
  function toCanonicalToolName$3(qwenName) {
94239
- return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
94447
+ return lookupOwn({
94448
+ record: QWEN_TO_CANONICAL_TOOL_NAMES,
94449
+ key: qwenName
94450
+ }) ?? qwenName;
94240
94451
  }
94241
94452
  function parseQwenPermissionEntry(entry, options = {}) {
94242
94453
  const parenIndex = entry.indexOf("(");
@@ -94852,15 +95063,23 @@ function convertQwenToRulesyncPermissions(params) {
94852
95063
  if (!parsed.ok) {
94853
95064
  if (action === "deny") {
94854
95065
  const canonical = toCanonicalToolName$3(parsed.toolName);
94855
- if (!permission[canonical]) permission[canonical] = {};
94856
- permission[canonical]["*"] = action;
95066
+ if (isPrototypePollutionKey(canonical)) continue;
95067
+ getOrCreateOwn({
95068
+ record: permission,
95069
+ key: canonical,
95070
+ create: () => ({})
95071
+ })["*"] = action;
94857
95072
  }
94858
95073
  continue;
94859
95074
  }
94860
95075
  const { toolName, pattern } = parsed;
94861
95076
  const canonical = toCanonicalToolName$3(toolName);
94862
- if (!permission[canonical]) permission[canonical] = {};
94863
- permission[canonical][pattern] = action;
95077
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
95078
+ getOrCreateOwn({
95079
+ record: permission,
95080
+ key: canonical,
95081
+ create: () => ({})
95082
+ })[pattern] = action;
94864
95083
  }
94865
95084
  };
94866
95085
  processEntries(params.allow, "allow");
@@ -94924,15 +95143,24 @@ const REASONIX_TO_CANONICAL_TOOL_NAMES = {
94924
95143
  };
94925
95144
  /** Every Reasonix tool name a canonical category owns on generate. */
94926
95145
  function reasonixToolNamesOf(canonical) {
94927
- const legacy = CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES[canonical];
95146
+ const legacy = lookupOwn({
95147
+ record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
95148
+ key: canonical
95149
+ });
94928
95150
  const current = toReasonixToolName(canonical);
94929
95151
  return legacy === void 0 ? [current] : [current, legacy];
94930
95152
  }
94931
95153
  function toReasonixToolName(canonical) {
94932
- return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
95154
+ return lookupOwn({
95155
+ record: CANONICAL_TO_REASONIX_TOOL_NAMES,
95156
+ key: canonical
95157
+ }) ?? canonical;
94933
95158
  }
94934
95159
  function toCanonicalToolName$2(reasonixName) {
94935
- return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
95160
+ return lookupOwn({
95161
+ record: REASONIX_TO_CANONICAL_TOOL_NAMES,
95162
+ key: reasonixName
95163
+ }) ?? reasonixName;
94936
95164
  }
94937
95165
  /**
94938
95166
  * Whether an entry uses the first-class `Tool=<literal>` exact-command form
@@ -95555,8 +95783,12 @@ function convertReasonixToRulesyncPermissions(params) {
95555
95783
  for (const entry of entries) {
95556
95784
  const { toolName, pattern } = parseToolPatternEntry(entry);
95557
95785
  const canonical = toCanonicalToolName$2(toolName);
95558
- if (!permission[canonical]) permission[canonical] = {};
95559
- permission[canonical][pattern] = action;
95786
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
95787
+ getOrCreateOwn({
95788
+ record: permission,
95789
+ key: canonical,
95790
+ create: () => ({})
95791
+ })[pattern] = action;
95560
95792
  }
95561
95793
  };
95562
95794
  processEntries(params.allow, "allow");
@@ -96108,7 +96340,10 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
96108
96340
  if (bash) toolPermissions.bash = bash;
96109
96341
  continue;
96110
96342
  }
96111
- const toolKeys = CATEGORY_TO_TOOL_KEYS[category];
96343
+ const toolKeys = lookupOwn({
96344
+ record: CATEGORY_TO_TOOL_KEYS,
96345
+ key: category
96346
+ });
96112
96347
  if (!toolKeys) {
96113
96348
  logger?.warn(`Rovo Dev permissions have no target for the "${category}" category. Skipping it.`);
96114
96349
  markSharedToolKeyRestriction({
@@ -96256,7 +96491,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
96256
96491
  }
96257
96492
  if (isStringArray(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
96258
96493
  permission.read ??= {};
96259
- permission.read[path] ??= "allow";
96494
+ if (!Object.hasOwn(permission.read, path)) permission.read[path] = "allow";
96260
96495
  }
96261
96496
  return { permission };
96262
96497
  }
@@ -97993,11 +98228,17 @@ var TraePermissions = class TraePermissions extends ToolPermissions {
97993
98228
  const permission = {};
97994
98229
  const put = (category, pattern, action) => {
97995
98230
  if (isPrototypePollutionKey(category) || isPrototypePollutionKey(pattern)) return;
97996
- const rules = permission[category] ??= {};
97997
- const existing = rules[pattern];
98231
+ const rules = getOrCreateOwn({
98232
+ record: permission,
98233
+ key: category,
98234
+ create: () => ({})
98235
+ });
97998
98236
  if (isStricterAction({
97999
98237
  action,
98000
- existing
98238
+ existing: lookupOwn({
98239
+ record: rules,
98240
+ key: pattern
98241
+ })
98001
98242
  })) rules[pattern] = action;
98002
98243
  };
98003
98244
  for (const [key, value] of Object.entries(readRecord(customProfiles, COMMAND_RULES_PATH))) {
@@ -100340,7 +100581,10 @@ function toZedToolName(canonical) {
100340
100581
  const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
100341
100582
  return `${MCP_ZED_PREFIX}${address}`;
100342
100583
  }
100343
- return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
100584
+ return lookupOwn({
100585
+ record: CANONICAL_TO_ZED_TOOL_NAMES,
100586
+ key: canonical
100587
+ }) ?? canonical;
100344
100588
  }
100345
100589
  function toCanonicalToolName(zedName) {
100346
100590
  if (zedName.startsWith(MCP_ZED_PREFIX)) {
@@ -100348,7 +100592,10 @@ function toCanonicalToolName(zedName) {
100348
100592
  const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
100349
100593
  return `${MCP_CATEGORY_PREFIX}${address}`;
100350
100594
  }
100351
- return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
100595
+ return lookupOwn({
100596
+ record: ZED_TO_CANONICAL_TOOL_NAMES,
100597
+ key: zedName
100598
+ }) ?? zedName;
100352
100599
  }
100353
100600
  /**
100354
100601
  * Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
@@ -100660,7 +100907,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
100660
100907
  const globalDefault = parsed.success ? parsed.data.default : void 0;
100661
100908
  const permission = {};
100662
100909
  const ensure = (category) => {
100663
- const existing = permission[category];
100910
+ const existing = lookupOwn({
100911
+ record: permission,
100912
+ key: category
100913
+ });
100664
100914
  if (existing) return existing;
100665
100915
  const created = {};
100666
100916
  permission[category] = created;
@@ -101287,7 +101537,7 @@ function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
101287
101537
  async function convertFromTool(params) {
101288
101538
  resetRunWarningState();
101289
101539
  const packagingTarget = [params.fromTool, ...params.toTools].find(isPackagingToolTarget);
101290
- if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import and generate with explicit plugin directories.`);
101540
+ 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).`);
101291
101541
  const ctx = params;
101292
101542
  const [rulesCount, ignoreCount, mcpCount, commandsCount, subagentsCount, skillsCount, hooksCount, permissionsCount, checksCount] = [
101293
101543
  await runFeatureConvert(ctx, buildRulesStrategy(ctx)),
@@ -104535,12 +104785,6 @@ Object.defineProperty(exports, "isFileSystemError", {
104535
104785
  return isFileSystemError;
104536
104786
  }
104537
104787
  });
104538
- Object.defineProperty(exports, "isPackagingToolTarget", {
104539
- enumerable: true,
104540
- get: function() {
104541
- return isPackagingToolTarget;
104542
- }
104543
- });
104544
104788
  Object.defineProperty(exports, "isRecord", {
104545
104789
  enumerable: true,
104546
104790
  get: function() {
@@ -104607,6 +104851,12 @@ Object.defineProperty(exports, "parseCommaSeparatedList", {
104607
104851
  return parseCommaSeparatedList;
104608
104852
  }
104609
104853
  });
104854
+ Object.defineProperty(exports, "parseConfigFileContent", {
104855
+ enumerable: true,
104856
+ get: function() {
104857
+ return parseConfigFileContent;
104858
+ }
104859
+ });
104610
104860
  Object.defineProperty(exports, "parseJsonc", {
104611
104861
  enumerable: true,
104612
104862
  get: function() {