rulesync 9.8.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,28 +114,35 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$250 } = node_path.posix;
117
+ const { join: join$251 } = node_path.posix;
118
118
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
119
119
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
120
120
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
121
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
121
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
+ const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
128
+ const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
129
+ const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
127
130
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
128
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
131
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
129
132
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
130
133
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
131
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$250(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
134
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "skills");
135
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$251(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
133
136
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
134
137
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
135
138
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
136
139
  const RULESYNC_PERMISSIONS_FILE_NAME = "permissions.json";
140
+ const RULESYNC_MCP_JSONC_FILE_NAME = "mcp.jsonc";
141
+ const RULESYNC_HOOKS_JSONC_FILE_NAME = "hooks.jsonc";
142
+ const RULESYNC_PERMISSIONS_JSONC_FILE_NAME = "permissions.jsonc";
137
143
  const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
138
144
  const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
145
+ const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
139
146
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
140
147
  //#endregion
141
148
  //#region src/types/tool-target-tuples.ts
@@ -7343,12 +7350,55 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
7343
7350
  return canonical;
7344
7351
  }
7345
7352
  //#endregion
7353
+ //#region src/utils/jsonc.ts
7354
+ /**
7355
+ * Rebuild the parsed value from its own enumerable entries, dropping
7356
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
7357
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
7358
+ * literal `__proto__` key would replace the containing object's prototype
7359
+ * instead of becoming an own property; rebuilding gives every object a clean
7360
+ * `Object.prototype` again and severs that path.
7361
+ */
7362
+ function deepSanitize(value) {
7363
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
7364
+ if (value !== null && typeof value === "object") {
7365
+ const sanitized = {};
7366
+ for (const [key, entry] of Object.entries(value)) {
7367
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
7368
+ sanitized[key] = deepSanitize(entry);
7369
+ }
7370
+ return sanitized;
7371
+ }
7372
+ return value;
7373
+ }
7374
+ /**
7375
+ * Parse a JSONC (JSON with Comments) document strictly.
7376
+ *
7377
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
7378
+ * errors and returns a best-effort value), this throws on any parse error so
7379
+ * a malformed source file fails loudly instead of generating half-empty tool
7380
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
7381
+ * `JSON.parse` on files that may contain comments or trailing commas.
7382
+ */
7383
+ function parseJsonc$6(content) {
7384
+ const errors = [];
7385
+ const result = (0, jsonc_parser.parse)(content, errors, {
7386
+ allowTrailingComma: true,
7387
+ disallowComments: false
7388
+ });
7389
+ if (errors.length > 0) {
7390
+ const details = errors.map((e) => `${(0, jsonc_parser.printParseErrorCode)(e.error)} at offset ${e.offset}`).join(", ");
7391
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
7392
+ }
7393
+ return deepSanitize(result);
7394
+ }
7395
+ //#endregion
7346
7396
  //#region src/features/hooks/rulesync-hooks.ts
7347
7397
  var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7348
7398
  json;
7349
7399
  constructor(params) {
7350
7400
  super({ ...params });
7351
- this.json = JSON.parse(this.fileContent);
7401
+ this.json = parseJsonc$6(this.fileContent);
7352
7402
  if (params.validate) {
7353
7403
  const result = this.validate();
7354
7404
  if (!result.success) throw result.error;
@@ -7357,7 +7407,11 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7357
7407
  static getSettablePaths() {
7358
7408
  return {
7359
7409
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7360
- relativeFilePath: "hooks.json"
7410
+ relativeFilePath: RULESYNC_HOOKS_FILE_NAME,
7411
+ jsonc: {
7412
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7413
+ relativeFilePath: RULESYNC_HOOKS_JSONC_FILE_NAME
7414
+ }
7361
7415
  };
7362
7416
  }
7363
7417
  validate() {
@@ -7373,16 +7427,23 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7373
7427
  }
7374
7428
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
7375
7429
  const paths = RulesyncHooks.getSettablePaths();
7376
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7377
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} found.`);
7378
- const fileContent = await readFileContent(filePath);
7379
- return new RulesyncHooks({
7380
- outputRoot,
7430
+ const candidates = [paths.jsonc, {
7381
7431
  relativeDirPath: paths.relativeDirPath,
7382
- relativeFilePath: paths.relativeFilePath,
7383
- fileContent,
7384
- validate
7385
- });
7432
+ relativeFilePath: paths.relativeFilePath
7433
+ }];
7434
+ for (const candidate of candidates) {
7435
+ const filePath = (0, node_path.join)(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
7436
+ if (!await fileExists(filePath)) continue;
7437
+ const fileContent = await readFileContent(filePath);
7438
+ return new RulesyncHooks({
7439
+ outputRoot,
7440
+ relativeDirPath: candidate.relativeDirPath,
7441
+ relativeFilePath: candidate.relativeFilePath,
7442
+ fileContent,
7443
+ validate
7444
+ });
7445
+ }
7446
+ throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} or ${RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH} found.`);
7386
7447
  }
7387
7448
  getJson() {
7388
7449
  return this.json;
@@ -12390,15 +12451,51 @@ const RulesyncMcpServerSchema = zod_mini.z.extend(McpServerSchema, {
12390
12451
  exposed: zod_mini.z.optional(zod_mini.z.boolean())
12391
12452
  });
12392
12453
  const RulesyncMcpConfigSchema = zod_mini.z.object({ mcpServers: zod_mini.z.record(zod_mini.z.string(), RulesyncMcpServerSchema) });
12454
+ /**
12455
+ * Tool-scoped MCP block: servers that apply only to one tool. A named entry
12456
+ * replaces/adds the same-named shared server wholesale for that tool; `null`
12457
+ * removes the shared server for that tool. Mirrors `{toolname}.hooks` in
12458
+ * `.rulesync/hooks.json` and `{toolname}.permission` in
12459
+ * `.rulesync/permissions.json`.
12460
+ */
12461
+ const toolScopedMcpSchema = zod_mini.z.looseObject({ mcpServers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.nullable(RulesyncMcpServerSchema))) });
12393
12462
  const RulesyncMcpFileSchema = zod_mini.z.looseObject({
12394
12463
  $schema: zod_mini.z.optional(zod_mini.z.string()),
12395
- ...RulesyncMcpConfigSchema.shape
12464
+ ...RulesyncMcpConfigSchema.shape,
12465
+ amp: zod_mini.z.optional(toolScopedMcpSchema),
12466
+ "antigravity-cli": zod_mini.z.optional(toolScopedMcpSchema),
12467
+ "antigravity-ide": zod_mini.z.optional(toolScopedMcpSchema),
12468
+ augmentcode: zod_mini.z.optional(toolScopedMcpSchema),
12469
+ claudecode: zod_mini.z.optional(toolScopedMcpSchema),
12470
+ cline: zod_mini.z.optional(toolScopedMcpSchema),
12471
+ codexcli: zod_mini.z.optional(toolScopedMcpSchema),
12472
+ copilot: zod_mini.z.optional(toolScopedMcpSchema),
12473
+ copilotcli: zod_mini.z.optional(toolScopedMcpSchema),
12474
+ cursor: zod_mini.z.optional(toolScopedMcpSchema),
12475
+ deepagents: zod_mini.z.optional(toolScopedMcpSchema),
12476
+ devin: zod_mini.z.optional(toolScopedMcpSchema),
12477
+ factorydroid: zod_mini.z.optional(toolScopedMcpSchema),
12478
+ goose: zod_mini.z.optional(toolScopedMcpSchema),
12479
+ grokcli: zod_mini.z.optional(toolScopedMcpSchema),
12480
+ hermesagent: zod_mini.z.optional(toolScopedMcpSchema),
12481
+ junie: zod_mini.z.optional(toolScopedMcpSchema),
12482
+ kilo: zod_mini.z.optional(toolScopedMcpSchema),
12483
+ kiro: zod_mini.z.optional(toolScopedMcpSchema),
12484
+ opencode: zod_mini.z.optional(toolScopedMcpSchema),
12485
+ qwencode: zod_mini.z.optional(toolScopedMcpSchema),
12486
+ reasonix: zod_mini.z.optional(toolScopedMcpSchema),
12487
+ roo: zod_mini.z.optional(toolScopedMcpSchema),
12488
+ rovodev: zod_mini.z.optional(toolScopedMcpSchema),
12489
+ takt: zod_mini.z.optional(toolScopedMcpSchema),
12490
+ vibe: zod_mini.z.optional(toolScopedMcpSchema),
12491
+ warp: zod_mini.z.optional(toolScopedMcpSchema),
12492
+ zed: zod_mini.z.optional(toolScopedMcpSchema)
12396
12493
  });
12397
12494
  var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12398
12495
  json;
12399
12496
  constructor(params) {
12400
12497
  super(params);
12401
- this.json = JSON.parse(this.fileContent);
12498
+ this.json = parseJsonc$6(this.fileContent);
12402
12499
  if (params.validate) {
12403
12500
  const result = this.validate();
12404
12501
  if (!result.success) throw result.error;
@@ -12408,7 +12505,11 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12408
12505
  return {
12409
12506
  recommended: {
12410
12507
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12411
- relativeFilePath: "mcp.json"
12508
+ relativeFilePath: RULESYNC_MCP_FILE_NAME
12509
+ },
12510
+ jsonc: {
12511
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12512
+ relativeFilePath: RULESYNC_MCP_JSONC_FILE_NAME
12412
12513
  },
12413
12514
  legacy: {
12414
12515
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
@@ -12430,7 +12531,18 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12430
12531
  static async fromFile({ outputRoot = process.cwd(), validate = true, logger }) {
12431
12532
  const paths = this.getSettablePaths();
12432
12533
  const recommendedPath = (0, node_path.join)(outputRoot, paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
12534
+ const jsoncPath = (0, node_path.join)(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
12433
12535
  const legacyPath = (0, node_path.join)(outputRoot, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
12536
+ if (await fileExists(jsoncPath)) {
12537
+ const fileContent = await readFileContent(jsoncPath);
12538
+ return new RulesyncMcp({
12539
+ outputRoot,
12540
+ relativeDirPath: paths.jsonc.relativeDirPath,
12541
+ relativeFilePath: paths.jsonc.relativeFilePath,
12542
+ fileContent,
12543
+ validate
12544
+ });
12545
+ }
12434
12546
  if (await fileExists(recommendedPath)) {
12435
12547
  const fileContent = await readFileContent(recommendedPath);
12436
12548
  return new RulesyncMcp({
@@ -12490,7 +12602,141 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12490
12602
  getJson() {
12491
12603
  return this.json;
12492
12604
  }
12493
- };
12605
+ /**
12606
+ * Build the effective MCP config for one tool target:
12607
+ *
12608
+ * 1. Filter shared servers by the DEPRECATED per-server `targets` field
12609
+ * (missing/`["*"]` means every tool). A deprecation warning points at
12610
+ * the tool-scoped blocks that replace it.
12611
+ * 2. Overlay the tool-scoped `{toolname}.mcpServers` block(s): a named
12612
+ * entry replaces/adds the shared server wholesale for this tool; `null`
12613
+ * removes it.
12614
+ *
12615
+ * Targets that share one output file resolve identically so the shared
12616
+ * file's content never depends on which of them generates last — see
12617
+ * `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
12618
+ * and the Antigravity pair).
12619
+ *
12620
+ * Returns the same instance when neither mechanism is used.
12621
+ */
12622
+ forTarget({ toolTarget, logger }) {
12623
+ const { blockKeys, acceptedTargetNames } = resolveMcpTarget({ toolTarget });
12624
+ const json = this.json;
12625
+ const sharedServers = this.json.mcpServers ?? {};
12626
+ const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
12627
+ if (serverNamesWithTargets.length > 0) this.warnTargetsDeprecationOnce({
12628
+ serverNamesWithTargets,
12629
+ logger
12630
+ });
12631
+ for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
12632
+ if (!isRecord(json[ignoredKey])) continue;
12633
+ this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
12634
+ }
12635
+ const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
12636
+ if (serverNamesWithTargets.length === 0 && toolBlockKeys.length === 0) return this;
12637
+ const effectiveServers = Object.fromEntries(Object.entries(sharedServers).filter(([, serverConfig]) => {
12638
+ const targets = serverConfig.targets;
12639
+ if (targets === void 0) return true;
12640
+ return targets.some((target) => target === "*" || acceptedTargetNames.has(target));
12641
+ }));
12642
+ for (const blockKey of blockKeys) {
12643
+ const toolBlock = json[blockKey];
12644
+ const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
12645
+ for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
12646
+ if (isPrototypePollutionKey(serverName)) continue;
12647
+ if (serverConfig === null) delete effectiveServers[serverName];
12648
+ else effectiveServers[serverName] = serverConfig;
12649
+ }
12650
+ }
12651
+ const rest = Object.fromEntries(Object.entries(json).filter(([key]) => !MCP_TOOL_BLOCK_KEYS.has(key)));
12652
+ return new RulesyncMcp({
12653
+ outputRoot: this.outputRoot,
12654
+ relativeDirPath: this.relativeDirPath,
12655
+ relativeFilePath: this.relativeFilePath,
12656
+ fileContent: JSON.stringify({
12657
+ ...rest,
12658
+ mcpServers: effectiveServers
12659
+ }, null, 2)
12660
+ });
12661
+ }
12662
+ /**
12663
+ * The deprecation warning would otherwise repeat once per generated tool
12664
+ * target (a full `--targets "*"` run creates one RulesyncMcp per target),
12665
+ * so it is deduplicated per source file path.
12666
+ */
12667
+ warnTargetsDeprecationOnce({ serverNamesWithTargets, logger }) {
12668
+ this.warnOncePerFile("targets-deprecation", `The per-server "targets" field in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is deprecated (servers: ${serverNamesWithTargets.join(", ")}). Author tool-scoped "{toolname}.mcpServers" blocks instead.`, logger);
12669
+ }
12670
+ warnOncePerFile(kind, message, logger) {
12671
+ const dedupeKey = `${this.getFilePath()}#${kind}`;
12672
+ if (warnedOnceKeys.has(dedupeKey)) return;
12673
+ warnedOnceKeys.add(dedupeKey);
12674
+ logger?.warn(message);
12675
+ }
12676
+ };
12677
+ /**
12678
+ * All keys that may hold a tool-scoped `{toolname}.mcpServers` block. Derived
12679
+ * from the MCP processor's target tuple so a newly added MCP-capable tool is
12680
+ * covered automatically.
12681
+ */
12682
+ const MCP_TOOL_BLOCK_KEYS = new Set(mcpProcessorToolTargetTuple);
12683
+ /**
12684
+ * Targets whose `{toolname}.mcpServers` block key is ALWAYS another target's
12685
+ * key (at every scope): `claudecode-legacy` is a deprecated alias of
12686
+ * `claudecode`, and the Kiro IDE/CLI targets share the `kiro` block because
12687
+ * all three write the same `.kiro/settings/mcp.json` at both scopes. Blocks
12688
+ * authored under these source names are never read (a warning is emitted).
12689
+ */
12690
+ const MCP_BLOCK_KEY_ALIASES = {
12691
+ "claudecode-legacy": "claudecode",
12692
+ "kiro-cli": "kiro",
12693
+ "kiro-ide": "kiro"
12694
+ };
12695
+ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
12696
+ /**
12697
+ * Resolve which tool-scoped blocks a target reads and which deprecated
12698
+ * `targets` names match it. Targets that share one output file must resolve
12699
+ * identically, otherwise the shared file's content would depend on which
12700
+ * target happened to generate last:
12701
+ *
12702
+ * - `claudecode-legacy` aliases `claudecode`; the Kiro trio shares `kiro`
12703
+ * (same output file at both scopes).
12704
+ * - `antigravity-ide` / `antigravity-cli` share their output file at BOTH
12705
+ * scopes — `.agents/mcp_config.json` (project) and
12706
+ * `~/.gemini/config/mcp_config.json` (global; both global subdirs are
12707
+ * `config`) — so both targets always apply both blocks in a fixed order
12708
+ * (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
12709
+ * per server on conflict).
12710
+ */
12711
+ function resolveMcpTarget({ toolTarget }) {
12712
+ if (toolTarget === "claudecode" || toolTarget === "claudecode-legacy") return {
12713
+ blockKeys: ["claudecode"],
12714
+ acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
12715
+ };
12716
+ if (toolTarget === "kiro" || toolTarget === "kiro-cli" || toolTarget === "kiro-ide") return {
12717
+ blockKeys: ["kiro"],
12718
+ acceptedTargetNames: /* @__PURE__ */ new Set([
12719
+ "kiro",
12720
+ "kiro-cli",
12721
+ "kiro-ide"
12722
+ ])
12723
+ };
12724
+ if (toolTarget === "antigravity-ide" || toolTarget === "antigravity-cli") return {
12725
+ blockKeys: ["antigravity-ide", "antigravity-cli"],
12726
+ acceptedTargetNames: /* @__PURE__ */ new Set(["antigravity-ide", "antigravity-cli"])
12727
+ };
12728
+ return {
12729
+ blockKeys: [toolTarget],
12730
+ acceptedTargetNames: /* @__PURE__ */ new Set([toolTarget])
12731
+ };
12732
+ }
12733
+ /**
12734
+ * Deduplication set for once-per-source-file warnings, keyed by
12735
+ * `<absolute file path>#<warning kind>`. Never cleared: rulesync CLI runs
12736
+ * are one-shot processes, and in a long-lived embedding (the rulesync MCP
12737
+ * server) repeating the same warning per generate would only add noise.
12738
+ */
12739
+ const warnedOnceKeys = /* @__PURE__ */ new Set();
12494
12740
  //#endregion
12495
12741
  //#region src/features/mcp/tool-mcp.ts
12496
12742
  var ToolMcp = class extends ToolFile {
@@ -16740,10 +16986,14 @@ var McpProcessor = class extends FeatureProcessor {
16740
16986
  if (!rulesyncMcp) throw new Error(`No ${RULESYNC_MCP_RELATIVE_FILE_PATH} found.`);
16741
16987
  const factory = this.getFactory(this.toolTarget);
16742
16988
  return await Promise.all([rulesyncMcp].map(async (mcp) => {
16989
+ const targetedRulesyncMcp = mcp.forTarget({
16990
+ toolTarget: this.toolTarget,
16991
+ logger: this.logger
16992
+ });
16743
16993
  const fieldsToStrip = [];
16744
16994
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
16745
16995
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
16746
- const filteredRulesyncMcp = mcp.stripMcpServerFields(fieldsToStrip);
16996
+ const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
16747
16997
  return await factory.class.fromRulesyncMcp({
16748
16998
  outputRoot: this.outputRoot,
16749
16999
  rulesyncMcp: filteredRulesyncMcp,
@@ -16793,6 +17043,25 @@ const PermissionActionSchema = zod_mini.z.enum([
16793
17043
  */
16794
17044
  const PermissionRulesSchema = zod_mini.z.record(zod_mini.z.string(), PermissionActionSchema);
16795
17045
  /**
17046
+ * Tool-scoped canonical `permission` block: the same shape as the shared
17047
+ * top-level `permission` record, but applied only to the tool whose override
17048
+ * key it appears under. During generation the categories placed here are
17049
+ * merged over the shared `permission` block per category (the tool-scoped
17050
+ * category replaces the shared one wholesale), mirroring how
17051
+ * `.rulesync/hooks.json` merges `{toolname}.hooks` per event.
17052
+ *
17053
+ * @example
17054
+ * { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
17055
+ */
17056
+ const ToolScopedPermissionSchema = zod_mini.z.record(zod_mini.z.string(), PermissionRulesSchema);
17057
+ /**
17058
+ * Generic tool-scoped override block for tools that have no tool-specific
17059
+ * override keys of their own; it carries only the canonical tool-scoped
17060
+ * `permission` block. Kept `looseObject` so future tool-specific keys can be
17061
+ * added without a schema break.
17062
+ */
17063
+ const CanonicalPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(ToolScopedPermissionSchema) });
17064
+ /**
16796
17065
  * OpenCode-specific permission value. Unlike the shared canonical block, which
16797
17066
  * only accepts a pattern-to-action map, OpenCode also allows a bare action
16798
17067
  * string that applies to the whole category (e.g. `"external_directory": "deny"`).
@@ -16822,7 +17091,7 @@ const OpencodePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: z
16822
17091
  * @example
16823
17092
  * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
16824
17093
  */
16825
- const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({});
17094
+ const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(ToolScopedPermissionSchema) });
16826
17095
  /**
16827
17096
  * Tool-scoped override block for Cline. Cline's `command-permissions.json`
16828
17097
  * carries a single global `allowRedirects` boolean (gates shell redirection
@@ -16834,7 +17103,10 @@ const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({});
16834
17103
  * @example
16835
17104
  * { "allowRedirects": true }
16836
17105
  */
16837
- const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({ allowRedirects: zod_mini.z.optional(zod_mini.z.boolean()) });
17106
+ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({
17107
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17108
+ allowRedirects: zod_mini.z.optional(zod_mini.z.boolean())
17109
+ });
16838
17110
  /**
16839
17111
  * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
16840
17112
  * object is a free-form record with tool-specific keys that have no canonical
@@ -16864,7 +17136,10 @@ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_m
16864
17136
  * @example
16865
17137
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
16866
17138
  */
16867
- const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({ permissions: zod_mini.z.optional(zod_mini.z.looseObject({})) });
17139
+ const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
17140
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17141
+ permissions: zod_mini.z.optional(zod_mini.z.looseObject({}))
17142
+ });
16868
17143
  /**
16869
17144
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
16870
17145
  * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
@@ -16896,6 +17171,7 @@ const VibePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_m
16896
17171
  * { "approvalMode": "auto-review" }
16897
17172
  */
16898
17173
  const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
17174
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16899
17175
  approvalMode: zod_mini.z.optional(zod_mini.z.string()),
16900
17176
  sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
16901
17177
  });
@@ -16921,6 +17197,7 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
16921
17197
  * }
16922
17198
  */
16923
17199
  const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
17200
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16924
17201
  tools: zod_mini.z.optional(zod_mini.z.looseObject({})),
16925
17202
  security: zod_mini.z.optional(zod_mini.z.looseObject({})),
16926
17203
  autoMode: zod_mini.z.optional(zod_mini.z.looseObject({}))
@@ -16942,6 +17219,7 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
16942
17219
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
16943
17220
  */
16944
17221
  const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
17222
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16945
17223
  sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
16946
17224
  agent: zod_mini.z.optional(zod_mini.z.looseObject({}))
16947
17225
  });
@@ -16961,7 +17239,10 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
16961
17239
  * @example
16962
17240
  * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
16963
17241
  */
16964
- const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({ commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) });
17242
+ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
17243
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17244
+ commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
17245
+ });
16965
17246
  /**
16966
17247
  * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
16967
17248
  * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
@@ -16978,6 +17259,7 @@ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({ commandBl
16978
17259
  * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
16979
17260
  */
16980
17261
  const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
17262
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16981
17263
  agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.string()),
16982
17264
  agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16983
17265
  agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean())
@@ -16995,6 +17277,7 @@ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
16995
17277
  * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16996
17278
  */
16997
17279
  const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
17280
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16998
17281
  allowReadonlyCommands: zod_mini.z.optional(zod_mini.z.boolean()),
16999
17282
  defaultBehavior: zod_mini.z.optional(zod_mini.z.string())
17000
17283
  });
@@ -17022,6 +17305,7 @@ const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
17022
17305
  * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
17023
17306
  */
17024
17307
  const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
17308
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17025
17309
  step_permission_overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
17026
17310
  provider_options: zod_mini.z.optional(zod_mini.z.looseObject({}))
17027
17311
  });
@@ -17049,6 +17333,7 @@ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
17049
17333
  * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
17050
17334
  */
17051
17335
  const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
17336
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17052
17337
  permissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
17053
17338
  tool: zod_mini.z.string(),
17054
17339
  action: zod_mini.z.string()
@@ -17077,6 +17362,7 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
17077
17362
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
17078
17363
  */
17079
17364
  const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
17365
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17080
17366
  toolPermission: zod_mini.z.optional(zod_mini.z.string()),
17081
17367
  enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
17082
17368
  });
@@ -17102,10 +17388,13 @@ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
17102
17388
  * "permission": { "type": "webhook-policy", "webhookUrl": "https://api.example.com/validate" } },
17103
17389
  * { "toolName": "view", "eventType": "tool-response", "permission": { "type": "allow" } } ] }
17104
17390
  */
17105
- const AugmentcodePermissionsOverrideSchema = zod_mini.z.looseObject({ toolPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
17106
- toolName: zod_mini.z.string(),
17107
- permission: zod_mini.z.looseObject({ type: zod_mini.z.string() })
17108
- }))) });
17391
+ const AugmentcodePermissionsOverrideSchema = zod_mini.z.looseObject({
17392
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17393
+ toolPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
17394
+ toolName: zod_mini.z.string(),
17395
+ permission: zod_mini.z.looseObject({ type: zod_mini.z.string() })
17396
+ })))
17397
+ });
17109
17398
  /**
17110
17399
  * Tool-scoped override block for Kiro. Kiro's agent config (`.kiro/agents/<name>.json`)
17111
17400
  * exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny
@@ -17131,21 +17420,24 @@ const AugmentcodePermissionsOverrideSchema = zod_mini.z.looseObject({ toolPermis
17131
17420
  * "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] },
17132
17421
  * "web_fetch": { "trusted": [".*github\\.com.*"] } } }
17133
17422
  */
17134
- const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zod_mini.z.optional(zod_mini.z.looseObject({
17135
- shell: zod_mini.z.optional(zod_mini.z.looseObject({
17136
- autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean()),
17137
- denyByDefault: zod_mini.z.optional(zod_mini.z.boolean())
17138
- })),
17139
- aws: zod_mini.z.optional(zod_mini.z.looseObject({
17140
- allowedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17141
- deniedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17142
- autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean())
17143
- })),
17144
- web_fetch: zod_mini.z.optional(zod_mini.z.looseObject({
17145
- trusted: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17146
- blocked: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
17423
+ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({
17424
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17425
+ toolsSettings: zod_mini.z.optional(zod_mini.z.looseObject({
17426
+ shell: zod_mini.z.optional(zod_mini.z.looseObject({
17427
+ autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean()),
17428
+ denyByDefault: zod_mini.z.optional(zod_mini.z.boolean())
17429
+ })),
17430
+ aws: zod_mini.z.optional(zod_mini.z.looseObject({
17431
+ allowedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17432
+ deniedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17433
+ autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean())
17434
+ })),
17435
+ web_fetch: zod_mini.z.optional(zod_mini.z.looseObject({
17436
+ trusted: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17437
+ blocked: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
17438
+ }))
17147
17439
  }))
17148
- })) });
17440
+ });
17149
17441
  /**
17150
17442
  * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
17151
17443
  * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
@@ -17217,6 +17509,7 @@ const CodexApprovalsReviewerSchema = zod_mini.z.enum([
17217
17509
  * "sandbox_workspace_write": { "network_access": true } }
17218
17510
  */
17219
17511
  const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
17512
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17220
17513
  approval_policy: zod_mini.z.optional(zod_mini.z.union([CodexApprovalPolicySchema, zod_mini.z.looseObject({})])),
17221
17514
  sandbox_mode: zod_mini.z.optional(CodexSandboxModeSchema),
17222
17515
  sandbox_workspace_write: zod_mini.z.optional(zod_mini.z.looseObject({})),
@@ -17235,6 +17528,15 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
17235
17528
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
17236
17529
  * block and ignores them.
17237
17530
  *
17531
+ * Additionally, every permissions-capable tool accepts a canonical tool-scoped
17532
+ * `permission` block under its override key (`{toolname}.permission`, same
17533
+ * shape as the shared `permission` record). Its categories apply only to that
17534
+ * tool, merged over the shared block per category — see
17535
+ * `RulesyncPermissions.forTarget`. `kiro-cli`/`kiro-ide` alias to the `kiro`
17536
+ * key and `hermesagent` to `hermes` (matching the shared output file each
17537
+ * writes). OpenCode/Kilo/Vibe keep their existing tool-native `permission`
17538
+ * override semantics (handled by their translators, not the central merge).
17539
+ *
17238
17540
  * @example
17239
17541
  * {
17240
17542
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
@@ -17260,7 +17562,13 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
17260
17562
  "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema),
17261
17563
  augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
17262
17564
  kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
17263
- codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema)
17565
+ codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
17566
+ "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17567
+ devin: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17568
+ goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17569
+ grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17570
+ rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17571
+ zed: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
17264
17572
  });
17265
17573
  /**
17266
17574
  * Full permissions file schema including optional $schema field.
@@ -17275,7 +17583,7 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
17275
17583
  json;
17276
17584
  constructor(params) {
17277
17585
  super({ ...params });
17278
- this.json = JSON.parse(this.fileContent);
17586
+ this.json = parseJsonc$6(this.fileContent);
17279
17587
  if (params.validate) {
17280
17588
  const result = this.validate();
17281
17589
  if (!result.success) throw result.error;
@@ -17284,7 +17592,11 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
17284
17592
  static getSettablePaths() {
17285
17593
  return {
17286
17594
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
17287
- relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME
17595
+ relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
17596
+ jsonc: {
17597
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
17598
+ relativeFilePath: RULESYNC_PERMISSIONS_JSONC_FILE_NAME
17599
+ }
17288
17600
  };
17289
17601
  }
17290
17602
  validate() {
@@ -17300,20 +17612,85 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
17300
17612
  }
17301
17613
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
17302
17614
  const paths = RulesyncPermissions.getSettablePaths();
17303
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17304
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
17305
- const fileContent = await readFileContent(filePath);
17306
- return new RulesyncPermissions({
17307
- outputRoot,
17615
+ const candidates = [paths.jsonc, {
17308
17616
  relativeDirPath: paths.relativeDirPath,
17309
- relativeFilePath: paths.relativeFilePath,
17310
- fileContent,
17311
- validate
17312
- });
17617
+ relativeFilePath: paths.relativeFilePath
17618
+ }];
17619
+ for (const candidate of candidates) {
17620
+ const filePath = (0, node_path.join)(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
17621
+ if (!await fileExists(filePath)) continue;
17622
+ const fileContent = await readFileContent(filePath);
17623
+ return new RulesyncPermissions({
17624
+ outputRoot,
17625
+ relativeDirPath: candidate.relativeDirPath,
17626
+ relativeFilePath: candidate.relativeFilePath,
17627
+ fileContent,
17628
+ validate
17629
+ });
17630
+ }
17631
+ throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} or ${RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH} found.`);
17313
17632
  }
17314
17633
  getJson() {
17315
17634
  return this.json;
17316
17635
  }
17636
+ /**
17637
+ * Build the effective permissions config for one tool target by merging the
17638
+ * tool-scoped `{toolname}.permission` block over the shared `permission`
17639
+ * block, per category (the tool-scoped category replaces the shared one
17640
+ * wholesale — mirroring how `{toolname}.hooks` merges per event in
17641
+ * `.rulesync/hooks.json`). The consumed `permission` key is stripped from
17642
+ * the override block so verbatim-passthrough translators (e.g. the Hermes
17643
+ * deep merge or the Codex CLI top-level key whitelist) never see it.
17644
+ *
17645
+ * Returns the same instance when the target has no tool-scoped canonical
17646
+ * `permission` block, or when the target's translator natively consumes its
17647
+ * own `permission` override shape (OpenCode / Kilo / Vibe).
17648
+ */
17649
+ forTarget({ toolTarget, logger }) {
17650
+ if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
17651
+ const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
17652
+ const json = this.json;
17653
+ if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
17654
+ const overrideBlock = json[overrideKey];
17655
+ if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
17656
+ const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
17657
+ const merged = {
17658
+ ...json,
17659
+ permission: {
17660
+ ...this.json.permission,
17661
+ ...toolScopedPermission
17662
+ }
17663
+ };
17664
+ if (Object.keys(restOverride).length > 0) merged[overrideKey] = restOverride;
17665
+ else delete merged[overrideKey];
17666
+ return new RulesyncPermissions({
17667
+ outputRoot: this.outputRoot,
17668
+ relativeDirPath: this.relativeDirPath,
17669
+ relativeFilePath: this.relativeFilePath,
17670
+ fileContent: JSON.stringify(merged, null, 2)
17671
+ });
17672
+ }
17673
+ };
17674
+ /**
17675
+ * Targets whose tool-scoped `permission` override uses tool-native semantics
17676
+ * consumed directly by their translator (bare action strings / tool-only
17677
+ * categories for OpenCode and Kilo, `sensitive_patterns` objects for Vibe).
17678
+ * The central canonical merge must not touch those blocks.
17679
+ */
17680
+ const NATIVE_PERMISSION_OVERRIDE_TARGETS = /* @__PURE__ */ new Set([
17681
+ "opencode",
17682
+ "kilo",
17683
+ "vibe"
17684
+ ]);
17685
+ /**
17686
+ * Targets that read their tool-scoped override from a differently named key:
17687
+ * Kiro IDE/CLI share the `kiro` block (they write the same agent config) and
17688
+ * Hermes Agent's established override key is `hermes`.
17689
+ */
17690
+ const PERMISSION_OVERRIDE_KEY_ALIASES = {
17691
+ "kiro-cli": "kiro",
17692
+ "kiro-ide": "kiro",
17693
+ hermesagent: "hermes"
17317
17694
  };
17318
17695
  //#endregion
17319
17696
  //#region src/features/permissions/tool-permissions.ts
@@ -19153,10 +19530,15 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
19153
19530
  existingDomainsHadUnknown,
19154
19531
  logger
19155
19532
  });
19156
- permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
19533
+ const profile = mergeWithExistingProfile({
19157
19534
  newProfile,
19158
19535
  existingProfile
19159
19536
  });
19537
+ permissionsTable[RULESYNC_PROFILE_NAME] = preserveUnmanagedProfileKeys({
19538
+ rawExistingProfile: permissionsTable[RULESYNC_PROFILE_NAME],
19539
+ profile,
19540
+ logger
19541
+ });
19160
19542
  const overridePatch = computeCodexcliOverridePatch({
19161
19543
  existing,
19162
19544
  override: rulesyncPermissions.getJson().codexcli,
@@ -19352,6 +19734,43 @@ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingD
19352
19734
  if (existingProfile?.network?.mode !== void 0) logger?.warn(`Preserving existing "network.mode" from config. Review this value manually as it may grant broader network access than the Rulesync-managed domain rules.`);
19353
19735
  if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19354
19736
  }
19737
+ const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
19738
+ "description",
19739
+ "extends",
19740
+ "filesystem",
19741
+ "network"
19742
+ ]);
19743
+ const MANAGED_NETWORK_KEYS = /* @__PURE__ */ new Set([
19744
+ "enabled",
19745
+ "mode",
19746
+ "domains",
19747
+ "unix_sockets"
19748
+ ]);
19749
+ /**
19750
+ * Re-attach the keys of the existing rulesync profile that rulesync does not
19751
+ * manage. The managed keys always come from `profile` (the freshly computed
19752
+ * merge result); unmanaged siblings — at the profile level and inside the
19753
+ * `network` table — are preserved verbatim from the existing config so a
19754
+ * regeneration never deletes user-authored Codex settings.
19755
+ */
19756
+ function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
19757
+ const rawProfileTable = toMutableTable(rawExistingProfile);
19758
+ const profileExtras = Object.fromEntries(Object.entries(rawProfileTable).filter(([key]) => !MANAGED_PROFILE_KEYS.has(key)));
19759
+ const rawNetworkTable = toMutableTable(rawProfileTable.network);
19760
+ const networkExtras = Object.fromEntries(Object.entries(rawNetworkTable).filter(([key]) => !MANAGED_NETWORK_KEYS.has(key)));
19761
+ const preservedKeyNames = [...Object.keys(profileExtras), ...Object.keys(networkExtras).map((key) => `network.${key}`)];
19762
+ if (preservedKeyNames.length > 0) logger?.warn(`Preserving unmanaged keys in the "${RULESYNC_PROFILE_NAME}" permissions profile: ${preservedKeyNames.join(", ")}. Review them manually; rulesync carries them forward verbatim.`);
19763
+ const result = {
19764
+ ...profileExtras,
19765
+ ...profile
19766
+ };
19767
+ const mergedNetwork = {
19768
+ ...networkExtras,
19769
+ ...profile.network
19770
+ };
19771
+ if (Object.keys(mergedNetwork).length > 0) result.network = mergedNetwork;
19772
+ return result;
19773
+ }
19355
19774
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
19356
19775
  if (!existingProfile) return newProfile;
19357
19776
  const mergedNetwork = { ...newProfile.network };
@@ -23613,16 +24032,20 @@ var PermissionsProcessor = class extends FeatureProcessor {
23613
24032
  if (!rulesyncPermissions) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
23614
24033
  const factory = toolPermissionsFactories.get(this.toolTarget);
23615
24034
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
24035
+ const effectivePermissions = rulesyncPermissions.forTarget({
24036
+ toolTarget: this.toolTarget,
24037
+ logger: this.logger
24038
+ });
23616
24039
  const toolPermissions = await factory.class.fromRulesyncPermissions({
23617
24040
  outputRoot: this.outputRoot,
23618
- rulesyncPermissions,
24041
+ rulesyncPermissions: effectivePermissions,
23619
24042
  logger: this.logger,
23620
24043
  global: this.global
23621
24044
  });
23622
24045
  if (this.toolTarget !== "codexcli") return [toolPermissions];
23623
24046
  return [toolPermissions, createCodexcliBashRulesFile({
23624
24047
  outputRoot: this.outputRoot,
23625
- config: rulesyncPermissions.getJson()
24048
+ config: effectivePermissions.getJson()
23626
24049
  })];
23627
24050
  }
23628
24051
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -39718,6 +40141,16 @@ async function generatePermissionsCore(params) {
39718
40141
  //#endregion
39719
40142
  //#region src/lib/import.ts
39720
40143
  /**
40144
+ * Import always writes the `.json` variant of a fixed-path source file, but a
40145
+ * sibling `.jsonc` variant takes precedence at read time — so an import into a
40146
+ * project that authors the `.jsonc` variant would be silently shadowed. Warn
40147
+ * so the user merges (or removes) one of the two by hand.
40148
+ */
40149
+ async function warnIfShadowedByJsonc(params) {
40150
+ const { outputRoot, jsonRelativePath, jsoncRelativePath, logger } = params;
40151
+ if (await fileExists((0, node_path.join)(outputRoot, jsoncRelativePath))) logger.warn(`${jsoncRelativePath} exists and takes precedence over the imported ${jsonRelativePath}. Merge the imported content into ${jsoncRelativePath} (or remove it) so the import takes effect.`);
40152
+ }
40153
+ /**
39721
40154
  * Import configuration files from AI tools.
39722
40155
  */
39723
40156
  async function importFromTool(params) {
@@ -39829,6 +40262,12 @@ async function importMcpCore(params) {
39829
40262
  }
39830
40263
  const rulesyncFiles = await mcpProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39831
40264
  const { count: writtenCount } = await mcpProcessor.writeAiFiles(rulesyncFiles);
40265
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40266
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40267
+ jsonRelativePath: RULESYNC_MCP_RELATIVE_FILE_PATH,
40268
+ jsoncRelativePath: RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH,
40269
+ logger
40270
+ });
39832
40271
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} MCP files`);
39833
40272
  return writtenCount;
39834
40273
  }
@@ -39928,6 +40367,12 @@ async function importHooksCore(params) {
39928
40367
  }
39929
40368
  const rulesyncFiles = await hooksProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39930
40369
  const { count: writtenCount } = await hooksProcessor.writeAiFiles(rulesyncFiles);
40370
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40371
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40372
+ jsonRelativePath: RULESYNC_HOOKS_RELATIVE_FILE_PATH,
40373
+ jsoncRelativePath: RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH,
40374
+ logger
40375
+ });
39931
40376
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} hooks file(s)`);
39932
40377
  return writtenCount;
39933
40378
  }
@@ -39957,6 +40402,12 @@ async function importPermissionsCore(params) {
39957
40402
  }
39958
40403
  const rulesyncFiles = await permissionsProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39959
40404
  const { count: writtenCount } = await permissionsProcessor.writeAiFiles(rulesyncFiles);
40405
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40406
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40407
+ jsonRelativePath: RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH,
40408
+ jsoncRelativePath: RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH,
40409
+ logger
40410
+ });
39960
40411
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} permissions file(s)`);
39961
40412
  return writtenCount;
39962
40413
  }
@@ -40021,6 +40472,12 @@ Object.defineProperty(exports, "CLIError", {
40021
40472
  return CLIError;
40022
40473
  }
40023
40474
  });
40475
+ Object.defineProperty(exports, "CODEXCLI_DIR", {
40476
+ enumerable: true,
40477
+ get: function() {
40478
+ return CODEXCLI_DIR;
40479
+ }
40480
+ });
40024
40481
  Object.defineProperty(exports, "CommandsProcessor", {
40025
40482
  enumerable: true,
40026
40483
  get: function() {
@@ -40117,6 +40574,12 @@ Object.defineProperty(exports, "RULESYNC_HOOKS_FILE_NAME", {
40117
40574
  return RULESYNC_HOOKS_FILE_NAME;
40118
40575
  }
40119
40576
  });
40577
+ Object.defineProperty(exports, "RULESYNC_HOOKS_JSONC_FILE_NAME", {
40578
+ enumerable: true,
40579
+ get: function() {
40580
+ return RULESYNC_HOOKS_JSONC_FILE_NAME;
40581
+ }
40582
+ });
40120
40583
  Object.defineProperty(exports, "RULESYNC_HOOKS_RELATIVE_FILE_PATH", {
40121
40584
  enumerable: true,
40122
40585
  get: function() {
@@ -40141,6 +40604,12 @@ Object.defineProperty(exports, "RULESYNC_MCP_FILE_NAME", {
40141
40604
  return RULESYNC_MCP_FILE_NAME;
40142
40605
  }
40143
40606
  });
40607
+ Object.defineProperty(exports, "RULESYNC_MCP_JSONC_FILE_NAME", {
40608
+ enumerable: true,
40609
+ get: function() {
40610
+ return RULESYNC_MCP_JSONC_FILE_NAME;
40611
+ }
40612
+ });
40144
40613
  Object.defineProperty(exports, "RULESYNC_MCP_RELATIVE_FILE_PATH", {
40145
40614
  enumerable: true,
40146
40615
  get: function() {
@@ -40165,12 +40634,24 @@ Object.defineProperty(exports, "RULESYNC_PERMISSIONS_FILE_NAME", {
40165
40634
  return RULESYNC_PERMISSIONS_FILE_NAME;
40166
40635
  }
40167
40636
  });
40637
+ Object.defineProperty(exports, "RULESYNC_PERMISSIONS_JSONC_FILE_NAME", {
40638
+ enumerable: true,
40639
+ get: function() {
40640
+ return RULESYNC_PERMISSIONS_JSONC_FILE_NAME;
40641
+ }
40642
+ });
40168
40643
  Object.defineProperty(exports, "RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH", {
40169
40644
  enumerable: true,
40170
40645
  get: function() {
40171
40646
  return RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH;
40172
40647
  }
40173
40648
  });
40649
+ Object.defineProperty(exports, "RULESYNC_PERMISSIONS_SCHEMA_URL", {
40650
+ enumerable: true,
40651
+ get: function() {
40652
+ return RULESYNC_PERMISSIONS_SCHEMA_URL;
40653
+ }
40654
+ });
40174
40655
  Object.defineProperty(exports, "RULESYNC_RELATIVE_DIR_PATH", {
40175
40656
  enumerable: true,
40176
40657
  get: function() {