rulesync 36.0.2 → 37.0.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.
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
457
457
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
458
458
  //#endregion
459
459
  //#region src/constants/rulesync-paths.ts
460
- const { join: join$422 } = node_path.posix;
460
+ const { join: join$423 } = node_path.posix;
461
461
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
462
462
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
463
463
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
464
464
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
465
465
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
466
466
  const RULES_FEATURE_SUBDIR = "rules";
467
- const CURATED_RULES_FEATURE_SUBDIR = join$422(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$423(RULES_FEATURE_SUBDIR, ".curated");
468
468
  const COMMANDS_FEATURE_SUBDIR = "commands";
469
469
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
470
470
  const CHECKS_FEATURE_SUBDIR = "checks";
471
471
  const SKILLS_FEATURE_SUBDIR = "skills";
472
- const CURATED_SKILLS_FEATURE_SUBDIR = join$422(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$422(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$423(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$423(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
484
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
485
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
486
486
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
487
487
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
488
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
490
490
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
491
491
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
492
492
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -4062,7 +4062,7 @@ function isPlainObject$1(value) {
4062
4062
  /**
4063
4063
  * Type guard to check if a value is an array of strings.
4064
4064
  */
4065
- function isStringArray$2(value) {
4065
+ function isStringArray$1(value) {
4066
4066
  return Array.isArray(value) && value.every((item) => typeof item === "string");
4067
4067
  }
4068
4068
  //#endregion
@@ -12857,6 +12857,10 @@ const SHARED_CONFIG_OWNERSHIP = {
12857
12857
  kind: "replace-owned-keys",
12858
12858
  ownedKeys: ["disableAllHooks"]
12859
12859
  },
12860
+ mcp: {
12861
+ kind: "replace-owned-keys",
12862
+ ownedKeys: ["disabledMcpServers"]
12863
+ },
12860
12864
  permissions: {
12861
12865
  kind: "replace-owned-keys",
12862
12866
  ownedKeys: ["deniedUrls"]
@@ -12871,6 +12875,10 @@ const SHARED_CONFIG_OWNERSHIP = {
12871
12875
  kind: "replace-owned-keys",
12872
12876
  ownedKeys: ["disableAllHooks"]
12873
12877
  },
12878
+ mcp: {
12879
+ kind: "replace-owned-keys",
12880
+ ownedKeys: ["disabledMcpServers"]
12881
+ },
12874
12882
  permissions: {
12875
12883
  kind: "replace-owned-keys",
12876
12884
  ownedKeys: ["allowedUrls", "deniedUrls"]
@@ -22135,7 +22143,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
22135
22143
  */
22136
22144
  function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
22137
22145
  if (Array.isArray(config.skills)) {
22138
- if (!layout.readsV2ConfigSpellings || !isStringArray$2(config.skills)) return [];
22146
+ if (!layout.readsV2ConfigSpellings || !isStringArray$1(config.skills)) return [];
22139
22147
  return config.skills.filter((value) => !/^https?:\/\//i.test(value));
22140
22148
  }
22141
22149
  const skills = asOpencodeEntries(config.skills);
@@ -26381,7 +26389,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
26381
26389
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
26382
26390
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
26383
26391
  const isEmittableString = ({ value }) => isNonEmptyString(value);
26384
- const isEmittableArray = ({ value }) => isStringArray$1(value);
26392
+ const isEmittableArray = ({ value }) => isStringArray(value);
26385
26393
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
26386
26394
  const isImportableArray = ({ value }) => isSafeStringArray(value);
26387
26395
  const isEmittableObject = ({ value }) => isPlainObject$1(value);
@@ -26823,7 +26831,7 @@ function isStringRecord(value) {
26823
26831
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
26824
26832
  return Object.values(value).every((v) => typeof v === "string");
26825
26833
  }
26826
- function isStringArray$1(value) {
26834
+ function isStringArray(value) {
26827
26835
  return Array.isArray(value) && value.every((v) => typeof v === "string");
26828
26836
  }
26829
26837
  /** Compare object values without letting key order decide the answer. */
@@ -26868,7 +26876,7 @@ function isSafeEnvEntry(entry) {
26868
26876
  * skipped wholesale when that read fails.
26869
26877
  */
26870
26878
  function isSafeStringArray(value) {
26871
- return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
26879
+ return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
26872
26880
  }
26873
26881
  /**
26874
26882
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -26910,7 +26918,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
26910
26918
  return {
26911
26919
  ...url !== void 0 && { url },
26912
26920
  ...isStringRecord(headers) && { headers },
26913
- ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
26921
+ ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
26914
26922
  };
26915
26923
  }
26916
26924
  if (hookType === "mcp_tool") {
@@ -28071,7 +28079,8 @@ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned,
28071
28079
  logger
28072
28080
  })).map((handler) => cloneHandler(handler));
28073
28081
  if (preserved.length === 0) continue;
28074
- const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher);
28082
+ const attributes = groupAttributesKey(group);
28083
+ const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher && groupAttributesKey(candidate) === attributes);
28075
28084
  if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
28076
28085
  else merged.push({
28077
28086
  ...omitPrototypePollutionKeys(group),
@@ -28192,6 +28201,11 @@ function stableStringify$1(value) {
28192
28201
  function matcherKey(matcher) {
28193
28202
  return matcher === void 0 ? "" : stableStringify$1(matcher);
28194
28203
  }
28204
+ /** The stable form of a matcher group's keys other than `matcher` and `hooks`. */
28205
+ function groupAttributesKey(group) {
28206
+ const { matcher: _matcher, hooks: _hooks, ...attributes } = group;
28207
+ return stableStringify$1(attributes);
28208
+ }
28195
28209
  function warnSkip({ logger, event, expected }) {
28196
28210
  logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
28197
28211
  }
@@ -28687,7 +28701,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
28687
28701
  function parseManifest(fileContent) {
28688
28702
  try {
28689
28703
  const parsed = JSON.parse(fileContent);
28690
- if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
28704
+ if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
28691
28705
  return {
28692
28706
  generatedBy: "rulesync",
28693
28707
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -30225,7 +30239,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
30225
30239
  * see the CLI config dir reference:
30226
30240
  * https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
30227
30241
  */
30228
- function copilotCliSettingsPaths({ global }) {
30242
+ function copilotCliSettingsPaths$1({ global }) {
30229
30243
  return global ? {
30230
30244
  relativeDirPath: COPILOT_DIR,
30231
30245
  relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
@@ -30240,7 +30254,7 @@ function copilotCliSettingsPaths({ global }) {
30240
30254
  * subject, and the permissions feature reports a broken settings file itself.
30241
30255
  */
30242
30256
  async function readDisableAllHooks({ outputRoot, global }) {
30243
- const paths = copilotCliSettingsPaths({ global });
30257
+ const paths = copilotCliSettingsPaths$1({ global });
30244
30258
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
30245
30259
  if (fileContent === null) return;
30246
30260
  try {
@@ -30270,7 +30284,7 @@ var CopilotcliHooksSettings = class CopilotcliHooksSettings extends ToolFile {
30270
30284
  return false;
30271
30285
  }
30272
30286
  static async fromDisableAllHooks({ outputRoot = process.cwd(), global = false, disableAllHooks }) {
30273
- const paths = copilotCliSettingsPaths({ global });
30287
+ const paths = copilotCliSettingsPaths$1({ global });
30274
30288
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30275
30289
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
30276
30290
  return new CopilotcliHooksSettings({
@@ -30300,7 +30314,7 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
30300
30314
  this.disableAllHooks = disableAllHooks;
30301
30315
  }
30302
30316
  static getExtraSharedWritePaths({ global = false } = {}) {
30303
- return [copilotCliSettingsPaths({ global })];
30317
+ return [copilotCliSettingsPaths$1({ global })];
30304
30318
  }
30305
30319
  static async getAuxiliaryFiles({ toolHooks } = {}) {
30306
30320
  return toolHooks instanceof CopilotcliHooks && toolHooks.settingsFile ? [toolHooks.settingsFile] : [];
@@ -34744,12 +34758,21 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
34744
34758
  validate
34745
34759
  });
34746
34760
  }
34747
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
34761
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
34748
34762
  const paths = QwencodeHooks.getSettablePaths({ global });
34749
34763
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
34750
34764
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
34751
34765
  const config = rulesyncHooks.getJson();
34752
- const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
34766
+ const previouslyOwned = preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0;
34767
+ const merged = mergeGeneratedHookLists({
34768
+ existingContent,
34769
+ generatedHooks: canonicalToQwencodeHooks(config, logger),
34770
+ shape: "matcher-groups",
34771
+ preserveUnowned,
34772
+ previouslyOwned,
34773
+ logger
34774
+ });
34775
+ const patch = { hooks: merged.hooks };
34753
34776
  const disableAllHooks = config.qwencode?.disableAllHooks;
34754
34777
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
34755
34778
  const fileContent = applySharedConfigPatch({
@@ -34764,9 +34787,16 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
34764
34787
  relativeDirPath: paths.relativeDirPath,
34765
34788
  relativeFilePath: paths.relativeFilePath,
34766
34789
  fileContent,
34790
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
34767
34791
  validate
34768
34792
  });
34769
34793
  }
34794
+ static supportsPreserveUnowned() {
34795
+ return true;
34796
+ }
34797
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
34798
+ return toolHooks instanceof QwencodeHooks ? toolHooks.getOwnershipLockFiles() : [];
34799
+ }
34770
34800
  toRulesyncHooks() {
34771
34801
  let settings;
34772
34802
  try {
@@ -41166,7 +41196,7 @@ function isCodexStdioServer(config) {
41166
41196
  * @see https://learn.chatgpt.com/docs/extend/mcp
41167
41197
  */
41168
41198
  function isValidRenamedArray(key, value) {
41169
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
41199
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
41170
41200
  }
41171
41201
  /**
41172
41202
  * The keys of Codex's `McpServerOAuthConfig`. Anything else in a server's
@@ -42385,18 +42415,6 @@ function renameEnabledToolsToTools({ server, serverName, logger }) {
42385
42415
  };
42386
42416
  }
42387
42417
  /**
42388
- * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
42389
- * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
42390
- * CLI-written entry can be a bare string. Canonical `enabledTools` is
42391
- * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
42392
- * through would reject the WHOLE imported MCP file rather than one key. Anything
42393
- * that is not a string array is therefore left under `tools` untouched, matching
42394
- * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
42395
- */
42396
- function isStringArray(value) {
42397
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
42398
- }
42399
- /**
42400
42418
  * Resolves and sets the transport type for each MCP server config.
42401
42419
  * GitHub Copilot CLI requires the "type" field for each server. A server it
42402
42420
  * cannot express — one that names no transport, or names one it carries no way
@@ -42438,7 +42456,7 @@ function addTypeField(mcpServers, logger) {
42438
42456
  });
42439
42457
  continue;
42440
42458
  }
42441
- const { httpUrl: _httpUrl, ...rest } = parsed;
42459
+ const { httpUrl: _httpUrl, disabled: _disabled, ...rest } = parsed;
42442
42460
  result[name] = {
42443
42461
  ...renameEnabledToolsToTools({
42444
42462
  server: rest,
@@ -42450,7 +42468,7 @@ function addTypeField(mcpServers, logger) {
42450
42468
  };
42451
42469
  continue;
42452
42470
  }
42453
- const { httpUrl: _localHttpUrl, ...local } = parsed;
42471
+ const { httpUrl: _localHttpUrl, disabled: _localDisabled, ...local } = parsed;
42454
42472
  const [head, ...tail] = resolveLocalMcpCommand(parsed);
42455
42473
  if (head === void 0) {
42456
42474
  warnAndSkipMcpServer({
@@ -42481,7 +42499,7 @@ function removeTypeField(config) {
42481
42499
  const result = {};
42482
42500
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
42483
42501
  const { tools, ...withoutTools } = server;
42484
- const restored = isStringArray(tools) ? {
42502
+ const restored = isStringArray$1(tools) ? {
42485
42503
  ...withoutTools,
42486
42504
  enabledTools: tools
42487
42505
  } : {
@@ -42497,6 +42515,99 @@ function removeTypeField(config) {
42497
42515
  }
42498
42516
  return result;
42499
42517
  }
42518
+ /**
42519
+ * The Copilot CLI settings file carrying `disabledMcpServers`:
42520
+ * `.github/copilot/settings.json` (project) / `~/.copilot/settings.json`
42521
+ * (global). Both scopes document the key ("MCP server names to disable. Listed
42522
+ * servers are configured but not started"); at repository scope it is
42523
+ * union-merged with the user value, so a repository can add names but never
42524
+ * remove them.
42525
+ *
42526
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
42527
+ */
42528
+ function copilotCliSettingsPaths({ global }) {
42529
+ return global ? {
42530
+ relativeDirPath: COPILOT_DIR,
42531
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
42532
+ } : {
42533
+ relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
42534
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
42535
+ };
42536
+ }
42537
+ async function readCopilotCliSettings({ outputRoot, global }) {
42538
+ const paths = copilotCliSettingsPaths({ global });
42539
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
42540
+ try {
42541
+ return {
42542
+ paths,
42543
+ existingContent,
42544
+ document: parseSharedConfig({
42545
+ format: "json",
42546
+ fileContent: existingContent,
42547
+ filePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath),
42548
+ invalidRootPolicy: "error"
42549
+ })
42550
+ };
42551
+ } catch (error) {
42552
+ return {
42553
+ paths,
42554
+ existingContent,
42555
+ document: null,
42556
+ error
42557
+ };
42558
+ }
42559
+ }
42560
+ /**
42561
+ * The names of the servers rulesync actually writes for this target: one
42562
+ * `addTypeField` skips (no transport, WebSocket, missing url or command) is
42563
+ * not managed, so its name neither gains a toggle nor strips a same-named
42564
+ * user-authored entry from the settings file.
42565
+ */
42566
+ function managedServerNames(mcpServers) {
42567
+ return Object.keys(addTypeField(mcpServers) ?? {});
42568
+ }
42569
+ /**
42570
+ * Overlays `disabled: true` on the servers the scope's settings file lists in
42571
+ * `disabledMcpServers`, so import round-trips the toggle into the canonical
42572
+ * config. A settings file that exists but cannot be parsed throws
42573
+ * (fail-closed): importing past it would re-enable every disabled server.
42574
+ */
42575
+ async function overlayDisabledServers({ mcpServers, outputRoot, global }) {
42576
+ const settings = await readCopilotCliSettings({
42577
+ outputRoot,
42578
+ global
42579
+ });
42580
+ if (settings.document === null) throw new Error(`Cannot import Copilot CLI MCP servers: ${formatError(settings.error)}. Fix the file so disabledMcpServers can be read.`, { cause: settings.error });
42581
+ const disabled = settings.document.disabledMcpServers;
42582
+ if (!isStringArray$1(disabled) || disabled.length === 0) return mcpServers;
42583
+ const servers = { ...mcpServers };
42584
+ for (const name of disabled) {
42585
+ if (!Object.hasOwn(servers, name)) continue;
42586
+ const server = servers[name];
42587
+ if (isPlainObject$1(server)) servers[name] = {
42588
+ ...server,
42589
+ disabled: true
42590
+ };
42591
+ }
42592
+ return servers;
42593
+ }
42594
+ /**
42595
+ * Auxiliary writer for `disabledMcpServers` in the Copilot CLI settings file.
42596
+ * Only that key is owned (through the shared-config gateway), so `deniedUrls`,
42597
+ * `disableAllHooks`, `model` and every other setting are preserved. It is a
42598
+ * shared, user-owned file and is never deleted.
42599
+ */
42600
+ var CopilotcliMcpSettings = class extends ToolFile {
42601
+ validate() {
42602
+ return {
42603
+ success: true,
42604
+ error: null
42605
+ };
42606
+ }
42607
+ isDeletable() {
42608
+ return false;
42609
+ }
42610
+ };
42500
42611
  var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42501
42612
  json;
42502
42613
  constructor(params) {
@@ -42539,7 +42650,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42539
42650
  const json = JSON.parse(fileContent);
42540
42651
  const newJson = {
42541
42652
  ...json,
42542
- mcpServers: json.mcpServers ?? {}
42653
+ mcpServers: await overlayDisabledServers({
42654
+ mcpServers: json.mcpServers ?? {},
42655
+ outputRoot,
42656
+ global
42657
+ })
42543
42658
  };
42544
42659
  return new CopilotcliMcp({
42545
42660
  outputRoot,
@@ -42554,7 +42669,15 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42554
42669
  const paths = this.getSettablePaths({ global });
42555
42670
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2);
42556
42671
  const json = JSON.parse(fileContent);
42557
- const copilotCliMcpServers = addTypeField(rulesyncMcp.getMcpServers(), logger);
42672
+ const settings = await readCopilotCliSettings({
42673
+ outputRoot,
42674
+ global
42675
+ });
42676
+ const copilotCliMcpServers = addTypeField(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).filter(([name, server]) => {
42677
+ if (server.disabled !== true || settings.document !== null) return true;
42678
+ logger?.warn(`[CopilotcliMcp] Skipping disabled MCP server "${name}": ${(0, node_path.join)(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} cannot be parsed, so disabledMcpServers cannot be written to switch it off.`);
42679
+ return false;
42680
+ })), logger);
42558
42681
  const mcpJson = {
42559
42682
  ...json,
42560
42683
  mcpServers: copilotCliMcpServers
@@ -42568,6 +42691,61 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42568
42691
  global
42569
42692
  });
42570
42693
  }
42694
+ /**
42695
+ * `disabledMcpServers` lives in the Copilot CLI settings file the hooks and
42696
+ * permissions features also write. Declared here so the write-order
42697
+ * derivation sees this feature as one of that file's writers — it is not a
42698
+ * settable path, since the servers themselves live in the MCP file.
42699
+ */
42700
+ static getExtraSharedWritePaths({ global = false } = {}) {
42701
+ return [copilotCliSettingsPaths({ global })];
42702
+ }
42703
+ /**
42704
+ * Routes the canonical `disabled: true` to `disabledMcpServers`, the key
42705
+ * Copilot CLI actually consults: an MCP file entry has no `disabled` field.
42706
+ * rulesync owns the toggle for the servers it writes; names it does not
42707
+ * manage keep their existing state. The file is not created or touched when
42708
+ * there is nothing to disable and nothing to retract.
42709
+ */
42710
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
42711
+ const servers = rulesyncMcp.forTarget({
42712
+ toolTarget: "copilotcli",
42713
+ global
42714
+ }).getMcpServers();
42715
+ const managedNames = managedServerNames(servers);
42716
+ const disabledNames = managedNames.filter((name) => servers[name]?.disabled === true);
42717
+ const settings = await readCopilotCliSettings({
42718
+ outputRoot,
42719
+ global
42720
+ });
42721
+ if (settings.document === null) {
42722
+ logger?.warn(`Skipping the Copilot CLI disabledMcpServers update: ${formatError(settings.error)}`);
42723
+ return [];
42724
+ }
42725
+ const existing = settings.document.disabledMcpServers;
42726
+ const existingDisabled = isStringArray$1(existing) ? existing : [];
42727
+ const managedNameSet = new Set(managedNames);
42728
+ const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
42729
+ if (reEnabled.length > 0) logger?.warn(`Copilot CLI MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
42730
+ const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
42731
+ if (mergedDisabled.length === 0 && existing === void 0) return [];
42732
+ if (isStringArray$1(existing) && existing.length === mergedDisabled.length && existing.toSorted().every((name, index) => name === mergedDisabled[index])) return [];
42733
+ const { paths } = settings;
42734
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
42735
+ return [new CopilotcliMcpSettings({
42736
+ outputRoot,
42737
+ relativeDirPath: paths.relativeDirPath,
42738
+ relativeFilePath: paths.relativeFilePath,
42739
+ fileContent: applySharedConfigPatch({
42740
+ fileKey: sharedConfigFileKey(paths),
42741
+ feature: "mcp",
42742
+ existingContent: settings.existingContent,
42743
+ patch: { disabledMcpServers: mergedDisabled.length > 0 ? mergedDisabled : void 0 },
42744
+ filePath
42745
+ }),
42746
+ global
42747
+ })];
42748
+ }
42571
42749
  toRulesyncMcp() {
42572
42750
  const mcpServers = removeTypeField(this.json);
42573
42751
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
@@ -42999,10 +43177,10 @@ function convertFromCrushFormat(crushServers) {
42999
43177
  if (value === "http" || value === "sse") converted.type = value;
43000
43178
  break;
43001
43179
  case "disabled_tools":
43002
- if (isStringArray$2(value)) converted.disabledTools = value;
43180
+ if (isStringArray$1(value)) converted.disabledTools = value;
43003
43181
  break;
43004
43182
  case "enabled_tools":
43005
- if (isStringArray$2(value)) converted.enabledTools = value;
43183
+ if (isStringArray$1(value)) converted.enabledTools = value;
43006
43184
  break;
43007
43185
  case "oauth_token": break;
43008
43186
  default: {
@@ -43989,7 +44167,7 @@ function convertRowsFromDsh(rows, logger) {
43989
44167
  } else {
43990
44168
  server.type = "stdio";
43991
44169
  if (typeof config.command === "string") server.command = config.command;
43992
- if (isStringArray$2(config.args)) server.args = config.args;
44170
+ if (isStringArray$1(config.args)) server.args = config.args;
43993
44171
  const env = copyStringRecord(config.env);
43994
44172
  if (env !== void 0) server.env = env;
43995
44173
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -44420,11 +44598,11 @@ function applyGooseStdioFields(ext, config) {
44420
44598
  if (Array.isArray(command)) {
44421
44599
  if (typeof command[0] === "string") ext.cmd = command[0];
44422
44600
  const rest = command.slice(1).filter((c) => typeof c === "string");
44423
- const args = isStringArray$2(config.args) ? config.args : [];
44601
+ const args = isStringArray$1(config.args) ? config.args : [];
44424
44602
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
44425
44603
  } else if (typeof command === "string") {
44426
44604
  ext.cmd = command;
44427
- if (isStringArray$2(config.args)) ext.args = config.args;
44605
+ if (isStringArray$1(config.args)) ext.args = config.args;
44428
44606
  }
44429
44607
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
44430
44608
  if (typeof config.cwd === "string") ext.cwd = config.cwd;
@@ -44527,7 +44705,7 @@ function convertFromGooseFormat(extensions) {
44527
44705
  else if (type === "streamable_http") server.type = "http";
44528
44706
  else server.type = "stdio";
44529
44707
  if (typeof ext.cmd === "string") server.command = ext.cmd;
44530
- if (isStringArray$2(ext.args)) server.args = ext.args;
44708
+ if (isStringArray$1(ext.args)) server.args = ext.args;
44531
44709
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
44532
44710
  if (typeof ext.cwd === "string") server.cwd = ext.cwd;
44533
44711
  if (typeof ext.uri === "string") server.url = ext.uri;
@@ -44549,11 +44727,11 @@ function buildGoosePluginStdioServer(config) {
44549
44727
  if (Array.isArray(command)) {
44550
44728
  if (typeof command[0] === "string") server.command = command[0];
44551
44729
  const rest = command.slice(1).filter((c) => typeof c === "string");
44552
- const args = isStringArray$2(config.args) ? config.args : [];
44730
+ const args = isStringArray$1(config.args) ? config.args : [];
44553
44731
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
44554
44732
  } else if (typeof command === "string") {
44555
44733
  server.command = command;
44556
- if (isStringArray$2(config.args)) server.args = config.args;
44734
+ if (isStringArray$1(config.args)) server.args = config.args;
44557
44735
  }
44558
44736
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
44559
44737
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -44961,7 +45139,7 @@ function copyHermesOauth(source) {
44961
45139
  if (!isRecord$1(source)) return;
44962
45140
  const oauth = {};
44963
45141
  for (const key of HERMES_OAUTH_STRING_KEYS) if (typeof source[key] === "string") oauth[key] = source[key];
44964
- if (typeof oauth.scope !== "string" && isStringArray$2(source.scopes)) oauth.scope = source.scopes.join(" ");
45142
+ if (typeof oauth.scope !== "string" && isStringArray$1(source.scopes)) oauth.scope = source.scopes.join(" ");
44965
45143
  for (const key of ["redirect_port", "timeout"]) if (typeof source[key] === "number") oauth[key] = source[key];
44966
45144
  if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
44967
45145
  return Object.keys(oauth).length > 0 ? oauth : void 0;
@@ -44988,7 +45166,7 @@ function copyHermesAdvancedFields(source, target) {
44988
45166
  target.auth = source.auth;
44989
45167
  copied = true;
44990
45168
  }
44991
- if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
45169
+ if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
44992
45170
  target.client_cert = source.client_cert;
44993
45171
  copied = true;
44994
45172
  }
@@ -45060,8 +45238,8 @@ function copyHermesAdvancedFields(source, target) {
45060
45238
  */
45061
45239
  function buildHermesToolsBlock(config) {
45062
45240
  const tools = {};
45063
- if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
45064
- if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
45241
+ if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
45242
+ if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
45065
45243
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
45066
45244
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
45067
45245
  return tools;
@@ -45073,8 +45251,8 @@ function buildHermesToolsBlock(config) {
45073
45251
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
45074
45252
  */
45075
45253
  function applyHermesToolsBlock(hermesTools, server) {
45076
- if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
45077
- if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
45254
+ if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
45255
+ if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
45078
45256
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
45079
45257
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
45080
45258
  }
@@ -45100,11 +45278,11 @@ function convertServerToHermes(config) {
45100
45278
  if (Array.isArray(command)) {
45101
45279
  if (typeof command[0] === "string") out.command = command[0];
45102
45280
  const rest = command.slice(1).filter((c) => typeof c === "string");
45103
- const args = isStringArray$2(config.args) ? config.args : [];
45281
+ const args = isStringArray$1(config.args) ? config.args : [];
45104
45282
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
45105
45283
  } else if (typeof command === "string") {
45106
45284
  out.command = command;
45107
- if (isStringArray$2(config.args)) out.args = config.args;
45285
+ if (isStringArray$1(config.args)) out.args = config.args;
45108
45286
  }
45109
45287
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
45110
45288
  } else if (url !== void 0) {
@@ -45154,7 +45332,7 @@ function convertFromHermesFormat(mcpServers) {
45154
45332
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
45155
45333
  const server = {};
45156
45334
  if (typeof config.command === "string") server.command = config.command;
45157
- if (isStringArray$2(config.args)) server.args = config.args;
45335
+ if (isStringArray$1(config.args)) server.args = config.args;
45158
45336
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
45159
45337
  if (typeof config.url === "string") server.url = config.url;
45160
45338
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -46346,7 +46524,7 @@ function mergeToolLists(...lists) {
46346
46524
  function toKiroMcpServers(servers) {
46347
46525
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46348
46526
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
46349
- const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
46527
+ const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
46350
46528
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
46351
46529
  return [name, {
46352
46530
  ...rest,
@@ -46370,7 +46548,7 @@ function fromKiroMcpServers(servers) {
46370
46548
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46371
46549
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
46372
46550
  const { autoApprove, ...rest } = server;
46373
- if (!isStringArray$2(autoApprove)) return [name, server];
46551
+ if (!isStringArray$1(autoApprove)) return [name, server];
46374
46552
  return [name, {
46375
46553
  ...rest,
46376
46554
  kiroAutoApprove: autoApprove
@@ -47799,7 +47977,7 @@ function headersRecordToPoolList(headers) {
47799
47977
  return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
47800
47978
  }
47801
47979
  function poolHeadersListToRecord(serverName, headers) {
47802
- if (!isStringArray$2(headers)) return void 0;
47980
+ if (!isStringArray$1(headers)) return void 0;
47803
47981
  const result = {};
47804
47982
  for (const entry of headers) {
47805
47983
  const separator = entry.indexOf(":");
@@ -47825,7 +48003,7 @@ const GLOB_METACHARACTERS = /[*?[]/;
47825
48003
  /** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
47826
48004
  const MAX_LISTED_GLOB_PATTERNS = 10;
47827
48005
  function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
47828
- if (!isStringArray$2(value)) {
48006
+ if (!isStringArray$1(value)) {
47829
48007
  warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
47830
48008
  return;
47831
48009
  }
@@ -48035,7 +48213,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
48035
48213
  const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
48036
48214
  return [serverName, {
48037
48215
  ...serverConfig,
48038
- ...isStringArray$2(poolAllow) && { poolAllow }
48216
+ ...isStringArray$1(poolAllow) && { poolAllow }
48039
48217
  }];
48040
48218
  })), logger);
48041
48219
  return new PoolMcp({
@@ -48266,7 +48444,7 @@ function pickServerLists(source) {
48266
48444
  if (!isPlainObject$1(source)) return {};
48267
48445
  return Object.fromEntries(QWENCODE_MCP_SERVER_LIST_KEYS.flatMap((key) => {
48268
48446
  const value = source[key];
48269
- return isStringArray$2(value) ? [[key, value]] : [];
48447
+ return isStringArray$1(value) ? [[key, value]] : [];
48270
48448
  }));
48271
48449
  }
48272
48450
  /**
@@ -48959,7 +49137,7 @@ async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger
48959
49137
  }
48960
49138
  function disabledNamesOf(config) {
48961
49139
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
48962
- return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
49140
+ return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
48963
49141
  }
48964
49142
  /**
48965
49143
  * The value `mcp.mcpConfigPath` needs so Rovo Dev reads the `mcp.json` rulesync
@@ -49375,7 +49553,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
49375
49553
  return [];
49376
49554
  }
49377
49555
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
49378
- const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
49556
+ const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
49379
49557
  const managedNameSet = new Set(managedNames);
49380
49558
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
49381
49559
  if (reEnabled.length > 0) logger?.warn(`Rovo Dev MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
@@ -56534,8 +56712,9 @@ function convertClaudeToRulesyncPermissions(params) {
56534
56712
  //#endregion
56535
56713
  //#region src/features/permissions/cline-permissions.ts
56536
56714
  /**
56537
- * Cline CLI loads command permissions from the `CLINE_COMMAND_PERMISSIONS` environment variable.
56538
- * Cline supports only `allow` and `deny` (no `ask`), and only for shell commands.
56715
+ * The legacy Cline CLI loaded command permissions from the `CLINE_COMMAND_PERMISSIONS`
56716
+ * environment variable. It supported only `allow` and `deny` (no `ask`), and only for
56717
+ * shell commands.
56539
56718
  * Schema:
56540
56719
  * ```json
56541
56720
  * { "allow": ["pattern1", ...], "deny": ["pattern2", ...], "allowRedirects": false }
@@ -56543,7 +56722,14 @@ function convertClaudeToRulesyncPermissions(params) {
56543
56722
  *
56544
56723
  * Rulesync writes the JSON to `.cline/command-permissions.json` so users can do:
56545
56724
  * `export CLINE_COMMAND_PERMISSIONS=$(cat .cline/command-permissions.json)`
56725
+ *
56726
+ * Deprecated: no Cline CLI 3 release reads `CLINE_COMMAND_PERMISSIONS`. Upstream removed
56727
+ * it from the docs in cline/cline@6354d209, and the CLI reference now says the CLI has no
56728
+ * allow or deny list for shell commands and recommends a `PreToolUse` hook instead
56729
+ * (https://docs.cline.bot/cli/cli-reference). The output is still generated for
56730
+ * compatibility, with a deprecation warning on every generation.
56546
56731
  */
56732
+ const CLINE_PERMISSIONS_DEPRECATION_WARNING = "WARNING: The Cline permissions output (.cline/command-permissions.json) is deprecated. No Cline CLI 3 release reads CLINE_COMMAND_PERMISSIONS, so the generated allow/deny lists are not enforced by current Cline releases. To block shell commands in Cline, use a preToolUse hook (rulesync hooks feature, cline target) that cancels the run_commands calls you do not allow. See https://docs.cline.bot/cli/cli-reference.";
56547
56733
  const ClineCommandPermissionsSchema = zod_mini.z.looseObject({
56548
56734
  allow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
56549
56735
  deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -56661,6 +56847,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
56661
56847
  });
56662
56848
  }
56663
56849
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
56850
+ logger?.warn(CLINE_PERMISSIONS_DEPRECATION_WARNING);
56664
56851
  const paths = ClinePermissions.getSettablePaths({ global });
56665
56852
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
56666
56853
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
@@ -56690,7 +56877,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
56690
56877
  const mergedDeny = (0, es_toolkit.uniq)([...existing.deny ?? [], ...dedupedDeny]).toSorted();
56691
56878
  const denySet = new Set(mergedDeny);
56692
56879
  const collisions = dedupedAllow.filter((p) => denySet.has(p));
56693
- if (collisions.length > 0) logger?.warn(`Cline command permissions: pattern(s) ${collisions.map((p) => `'${p}'`).join(", ")} appear in both 'allow' and 'deny'. Cline documents that deny rules always take precedence, so the 'allow' entry has no effect. Consider removing the duplicate rule.`);
56880
+ if (collisions.length > 0) logger?.warn(`Cline command permissions: pattern(s) ${collisions.map((p) => `'${p}'`).join(", ")} appear in both 'allow' and 'deny'. The legacy Cline CLI gave deny rules precedence, so the 'allow' entry has no effect. Consider removing the duplicate rule.`);
56694
56881
  const next = {
56695
56882
  ...existing,
56696
56883
  allow: dedupedAllow,
@@ -59973,9 +60160,9 @@ function managedContinueToolNames(config) {
59973
60160
  */
59974
60161
  function parseContinuePermissionLists(parsed) {
59975
60162
  return {
59976
- exclude: isStringArray$2(parsed.exclude) ? parsed.exclude : [],
59977
- ask: isStringArray$2(parsed.ask) ? parsed.ask : [],
59978
- allow: isStringArray$2(parsed.allow) ? parsed.allow : []
60163
+ exclude: isStringArray$1(parsed.exclude) ? parsed.exclude : [],
60164
+ ask: isStringArray$1(parsed.ask) ? parsed.ask : [],
60165
+ allow: isStringArray$1(parsed.allow) ? parsed.allow : []
59979
60166
  };
59980
60167
  }
59981
60168
  /**
@@ -60502,8 +60689,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
60502
60689
  logger
60503
60690
  });
60504
60691
  const managedToolNames = managedCrushToolNames(config);
60505
- const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
60506
- const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
60692
+ const preservedAllowed = (isStringArray$1(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
60693
+ const preservedDisabled = (isStringArray$1(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
60507
60694
  const allowedList = uniq$11([...preservedAllowed, ...generated.allowed]);
60508
60695
  const disabledList = uniq$11([...preservedDisabled, ...generated.disabled]);
60509
60696
  const patch = {};
@@ -60529,8 +60716,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
60529
60716
  const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
60530
60717
  const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
60531
60718
  const rulesyncConfig = convertCrushListsToRulesync({
60532
- allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
60533
- disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
60719
+ allowed: isStringArray$1(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
60720
+ disabled: isStringArray$1(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
60534
60721
  });
60535
60722
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
60536
60723
  }
@@ -62705,7 +62892,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
62705
62892
  function convertGoosePermissionConfigToRulesync(userPermission) {
62706
62893
  const permission = {};
62707
62894
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
62708
- const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
62895
+ const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
62709
62896
  const action = GOOSE_LIST_TO_ACTION[key];
62710
62897
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
62711
62898
  permission[category] ??= {};
@@ -63053,7 +63240,7 @@ const ACTION_RANK$3 = {
63053
63240
  * (mirrors the Cursor adapter's preservation of unmanaged types).
63054
63241
  */
63055
63242
  function unmanagedEntries(existingPermission, key) {
63056
- return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
63243
+ return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
63057
63244
  }
63058
63245
  /**
63059
63246
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -63110,7 +63297,7 @@ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger })
63110
63297
  const generated = ranked.get(GROK_ALL_TOOLS_ENTRY);
63111
63298
  for (const action of ["deny", "ask"]) {
63112
63299
  const entries = existingPermission[action];
63113
- if (!isStringArray$2(entries)) continue;
63300
+ if (!isStringArray$1(entries)) continue;
63114
63301
  if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63115
63302
  if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
63116
63303
  logger.warn(`Grok CLI permissions: removing the existing bare '*' entry from the '${action}' array — Rulesync now manages it through the all-tools '*' category, and the canonical config has no '*' '${action}' rule. Add "*": { "*": "${action}" } to keep restricting every tool.`);
@@ -63141,9 +63328,9 @@ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63141
63328
  * described more than once resolves to the strictest action.
63142
63329
  */
63143
63330
  function parseGrokPermissionArrays(permission) {
63144
- const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
63145
- const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
63146
- const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
63331
+ const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
63332
+ const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
63333
+ const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
63147
63334
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
63148
63335
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
63149
63336
  const result = {};
@@ -63351,20 +63538,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
63351
63538
  const permission = clonePermissionBlock(provenance.permission);
63352
63539
  reconcileCommandAllowlist({
63353
63540
  permission,
63354
- commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
63541
+ commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
63355
63542
  });
63356
63543
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
63357
63544
  reconcileNativeDenies({
63358
63545
  permission,
63359
63546
  category: SHELL_PERMISSION_CATEGORY,
63360
- patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
63547
+ patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
63361
63548
  });
63362
63549
  const security = isRecord$1(config.security) ? config.security : {};
63363
63550
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
63364
63551
  reconcileNativeDenies({
63365
63552
  permission,
63366
63553
  category: WEBFETCH_PERMISSION_CATEGORY,
63367
- patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
63554
+ patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
63368
63555
  });
63369
63556
  removeEmptyCategories(permission);
63370
63557
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -64758,13 +64945,13 @@ function classifyExistingEntry({ emitted, capability, pattern, effect }) {
64758
64945
  * how many entries rulesync does not generate.
64759
64946
  */
64760
64947
  function mergeExistingRule({ rule, emitted, logger }) {
64761
- if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$2(rule.match)) return {
64948
+ if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$1(rule.match)) return {
64762
64949
  rule,
64763
64950
  unrelatedEntries: 0
64764
64951
  };
64765
64952
  const capability = rule.capability;
64766
64953
  const isCatchAll = rule.match === void 0;
64767
- const patterns = isStringArray$2(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
64954
+ const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
64768
64955
  if (patterns.length === 0) return {
64769
64956
  rule,
64770
64957
  unrelatedEntries: 0
@@ -64948,7 +65135,7 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
64948
65135
  for (const rule of rules) {
64949
65136
  if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
64950
65137
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
64951
- const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$2(rule.match) ? rule.match : [];
65138
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$1(rule.match) ? rule.match : [];
64952
65139
  const categoryRules = permission[category] ??= {};
64953
65140
  for (const pattern of patterns) {
64954
65141
  const previous = categoryRules[pattern];
@@ -66666,8 +66853,8 @@ function importPoolTool({ toolConfig, bucket }) {
66666
66853
  bucket[CATCH_ALL_PATTERN$3] = "deny";
66667
66854
  return;
66668
66855
  }
66669
- const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
66670
- const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
66856
+ const denied = isStringArray$1(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
66857
+ const allowed = isStringArray$1(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
66671
66858
  for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
66672
66859
  for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
66673
66860
  }
@@ -68872,7 +69059,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
68872
69059
  permission[category] ??= {};
68873
69060
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
68874
69061
  }
68875
- if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
69062
+ if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
68876
69063
  permission.read ??= {};
68877
69064
  permission.read[path] ??= "allow";
68878
69065
  }
@@ -69128,7 +69315,7 @@ function stripRefusedOverridePaths(override) {
69128
69315
  */
69129
69316
  function collectTrustAffectingOverrideEntries({ tools, general }) {
69130
69317
  const entries = [];
69131
- const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
69318
+ const allowed = isStringArray$1(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
69132
69319
  if (allowed.length > 0) entries.push({
69133
69320
  label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
69134
69321
  reason: "auto-approves what it names as the override spells it; a non-shell entry is only checked against a whole-tool deny of its tool"
@@ -69279,7 +69466,7 @@ function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logge
69279
69466
  * hand-written entry went.
69280
69467
  */
69281
69468
  function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
69282
- const dropped = (key, list) => (0, es_toolkit.uniq)(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69469
+ const dropped = (key, list) => (0, es_toolkit.uniq)(isStringArray$1(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69283
69470
  if (list.includes(entry)) return false;
69284
69471
  const toolName = parseTabnineEntry(entry)?.toolName;
69285
69472
  return toolName === void 0 || !list.includes(toolName);
@@ -69361,7 +69548,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69361
69548
  }
69362
69549
  const overrideList = (key) => {
69363
69550
  const value = overrideTools[key];
69364
- if (value === void 0 || isStringArray$2(value)) return value ?? [];
69551
+ if (value === void 0 || isStringArray$1(value)) return value ?? [];
69365
69552
  warnWithFallback(logger, `Tabnine CLI permissions: ignored tools.${key} of the tabnine override (${quoteValueForWarning(value)}); it must be a list of tool names.`);
69366
69553
  return [];
69367
69554
  };
@@ -69399,7 +69586,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69399
69586
  if (ignoredOverrideKeys.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: ignored ${ignoredOverrideKeys.length} key(s) of the tabnine override (${ignoredOverrideKeys.map(quoteValueForWarning).join(", ")}); only 'tools' and 'general' are written through the permissions feature.`);
69400
69587
  const existingTools = this.existingToolsGroup(existingContent);
69401
69588
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => category === "bash" || category === "*" ? SHELL_TOOL_NAME : toTabnineToolName(category)));
69402
- const preservedEntries = (key) => (isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69589
+ const preservedEntries = (key) => (isStringArray$1(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69403
69590
  const toolName = parseTabnineEntry(entry)?.toolName;
69404
69591
  return toolName === void 0 || !managedToolNames.has(toolName);
69405
69592
  });
@@ -69477,7 +69664,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69477
69664
  if (rules !== void 0 && !Object.hasOwn(rules, pattern)) rules[pattern] = action;
69478
69665
  };
69479
69666
  for (const [key, action] of [[EXCLUDE_KEY, "deny"], [ALLOWED_KEY, "allow"]]) {
69480
- const entries = isStringArray$2(tools[key]) ? tools[key] : [];
69667
+ const entries = isStringArray$1(tools[key]) ? tools[key] : [];
69481
69668
  for (const entry of entries) {
69482
69669
  const parsed = parseTabnineEntry(entry);
69483
69670
  if (parsed === void 0) {
@@ -70345,12 +70532,12 @@ const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|\p{Emoji_Modifier_Base}|
70345
70532
  * The ranges are the `East_Asian_Width=A` property of Unicode 17.0, taken from
70346
70533
  * the table `get-east-asian-width` 1.6.0 carries and merged where two are
70347
70534
  * adjacent. The table is written out here rather than read from the package so
70348
- * that the one lookup adds no dependency of its own, and the drift test in
70349
- * `display-width.test.ts` is what keeps it honest: it loads the package through
70350
- * the prompt renderer that already depends on it, walks every code point, and
70351
- * fails when this table and the property disagree in either direction. When
70352
- * the renderer's copy moves to a newer Unicode, that test is what fails, and
70353
- * the table is regenerated from the new property — its ranges, marks aside,
70535
+ * that the one lookup adds no runtime dependency of its own, and the drift test
70536
+ * in `display-width.test.ts` is what keeps it honest: it loads the package,
70537
+ * pinned as a dev dependency, walks every code point, and fails when this
70538
+ * table and the property disagree in either direction. When that pin moves to
70539
+ * a newer Unicode, that test is what fails, and the table is regenerated from
70540
+ * the new property — its ranges, marks aside,
70354
70541
  * merged where adjacent and written as escapes — with the version named here.
70355
70542
  *
70356
70543
  * Three stretches of the property are left out because they never reach this
@@ -72099,8 +72286,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
72099
72286
  const executionProfiles = isRecord$1(agents["execution_profiles"]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
72100
72287
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles["default"]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
72101
72288
  const config = convertWarpToRulesyncPermissions({
72102
- allow: defaultProfile ? isStringArray$2(defaultProfile["command_allowlist"]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
72103
- deny: defaultProfile ? isStringArray$2(defaultProfile["command_denylist"]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
72289
+ allow: defaultProfile ? isStringArray$1(defaultProfile["command_allowlist"]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
72290
+ deny: defaultProfile ? isStringArray$1(defaultProfile["command_denylist"]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
72104
72291
  });
72105
72292
  const warpOverride = {};
72106
72293
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -72630,8 +72817,8 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
72630
72817
  const allowlist = defaultProfile[PROFILE_ALLOWLIST_KEY];
72631
72818
  const denylist = defaultProfile[PROFILE_DENYLIST_KEY];
72632
72819
  const config = convertWarpToRulesyncPermissions({
72633
- allow: isStringArray$2(allowlist) ? allowlist : [],
72634
- deny: isStringArray$2(denylist) ? denylist : []
72820
+ allow: isStringArray$1(allowlist) ? allowlist : [],
72821
+ deny: isStringArray$1(denylist) ? denylist : []
72635
72822
  });
72636
72823
  const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
72637
72824
  const warpcliOverride = {};
@@ -84646,7 +84833,7 @@ var BobSubagent = class extends RooSubagent {
84646
84833
  const mode = super.toRooMode(rulesyncSubagent);
84647
84834
  const frontmatter = rulesyncSubagent.getFrontmatter();
84648
84835
  const bobSection = isRecord$1(frontmatter.bob) ? frontmatter.bob : {};
84649
- if (isStringArray$2(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
84836
+ if (isStringArray$1(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
84650
84837
  return mode;
84651
84838
  }
84652
84839
  };
@@ -90101,7 +90288,7 @@ var ZoocodeSubagent = class extends RooSubagent {
90101
90288
  const mode = super.toRooMode(rulesyncSubagent);
90102
90289
  const frontmatter = rulesyncSubagent.getFrontmatter();
90103
90290
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
90104
- if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
90291
+ if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
90105
90292
  return mode;
90106
90293
  }
90107
90294
  toRulesyncSubagents() {
@@ -90113,7 +90300,7 @@ var ZoocodeSubagent = class extends RooSubagent {
90113
90300
  targets: ["zoocode"],
90114
90301
  roo: restRooSection
90115
90302
  };
90116
- if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
90303
+ if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
90117
90304
  return new RulesyncSubagent({
90118
90305
  outputRoot: ".",
90119
90306
  frontmatter: rebuilt,
@@ -94054,7 +94241,8 @@ var NestedAgentsmdRule = class extends ToolRule {
94054
94241
  * `<subprojectPath>/AGENTS.md` (project scope only); every other non-root rule
94055
94242
  * folds into the root `AGENTS.md`, since Codex has no modular rules directory.
94056
94243
  * The project files on the root-to-cwd chain share one `project_doc_max_bytes`
94057
- * budget (32 KiB by default), which the RulesProcessor warns about.
94244
+ * budget (32 KiB by default, or the value set in the project's
94245
+ * `.codex/config.toml`), which the RulesProcessor warns about.
94058
94246
  *
94059
94247
  * @see https://learn.chatgpt.com/docs/agent-configuration/agents-md
94060
94248
  */
@@ -94066,6 +94254,29 @@ var CodexcliRule = class extends NestedAgentsmdRule {
94066
94254
  };
94067
94255
  }
94068
94256
  };
94257
+ /**
94258
+ * Read the `project_doc_max_bytes` value set in the project's
94259
+ * `.codex/config.toml`, which Codex loads for trusted projects and which
94260
+ * overrides the 32 KiB default budget for project `AGENTS.md` files.
94261
+ *
94262
+ * Answers `undefined` when the file is missing, is not valid TOML, or does not
94263
+ * set the key to a positive integer, so the caller falls back to the default.
94264
+ *
94265
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
94266
+ */
94267
+ async function readCodexcliProjectDocMaxBytes(outputRoot) {
94268
+ const content = await readFileContentOrNull((0, node_path.join)(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME));
94269
+ if (content === null) return;
94270
+ let parsed;
94271
+ try {
94272
+ parsed = smol_toml.parse(content);
94273
+ } catch {
94274
+ return;
94275
+ }
94276
+ if (!isPlainObject$1(parsed)) return;
94277
+ const value = parsed.project_doc_max_bytes;
94278
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
94279
+ }
94069
94280
  //#endregion
94070
94281
  //#region src/features/rules/commandcode-rule.ts
94071
94282
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
@@ -99440,7 +99651,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
99440
99651
  collisionPolicy: "fold",
99441
99652
  projectInstructionBudget: {
99442
99653
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
99443
- remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (ignore this warning if you already have)."
99654
+ remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml`.",
99655
+ readConfiguredBytes: readCodexcliProjectDocMaxBytes,
99656
+ configuredSource: "`project_doc_max_bytes` in `.codex/config.toml`"
99444
99657
  }
99445
99658
  }
99446
99659
  }],
@@ -99985,7 +100198,7 @@ var RulesProcessor = class extends FeatureProcessor {
99985
100198
  factory
99986
100199
  });
99987
100200
  await this.warnForWarpLegacyRootFile(toolRules);
99988
- this.warnForInstructionBudget({
100201
+ await this.warnForInstructionBudget({
99989
100202
  toolRules,
99990
100203
  meta
99991
100204
  });
@@ -100006,14 +100219,19 @@ var RulesProcessor = class extends FeatureProcessor {
100006
100219
  * once, at its shallowest directory. With several output roots the warning
100007
100220
  * names the root it refers to. An output root below the git root is measured
100008
100221
  * without the parent directories' files Codex would also load, so the
100009
- * estimate can come out low there. The budget checked is the tool's default: a value
100010
- * raised by hand in the tool's own config is not read. Global scope is not
100222
+ * estimate can come out low there. The budget checked is the value set in the
100223
+ * output root's tool config when the tool supports one (Codex's
100224
+ * `project_doc_max_bytes` in `.codex/config.toml`), else the tool's default;
100225
+ * the warning names which one it used. Global scope is not
100011
100226
  * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
100012
100227
  * project budget.
100013
100228
  */
100014
- warnForInstructionBudget({ toolRules, meta }) {
100229
+ async warnForInstructionBudget({ toolRules, meta }) {
100015
100230
  const budget = meta.projectInstructionBudget;
100016
100231
  if (budget === void 0 || this.global) return;
100232
+ const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
100233
+ const limitBytes = configuredBytes ?? budget.bytes;
100234
+ const limitLabel = budget.readConfiguredBytes === void 0 ? `${limitBytes}-byte limit` : configuredBytes === void 0 ? `${limitBytes}-byte default limit` : `${limitBytes}-byte limit set by ${budget.configuredSource ?? "the tool config"}`;
100017
100235
  const files = toolRules.map((rule) => ({
100018
100236
  dir: normalizeRelativeDir(rule.getRelativeDirPath()),
100019
100237
  path: toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
@@ -100023,8 +100241,8 @@ var RulesProcessor = class extends FeatureProcessor {
100023
100241
  const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
100024
100242
  const rootFiles = files.filter(({ dir }) => dir === ".");
100025
100243
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100026
- if (rootBytes > budget.bytes) {
100027
- this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${budget.bytes}-byte limit. ${budget.remedy}`);
100244
+ if (rootBytes > limitBytes) {
100245
+ this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${budget.remedy}`);
100028
100246
  return;
100029
100247
  }
100030
100248
  const reportedDirs = [];
@@ -100033,9 +100251,9 @@ var RulesProcessor = class extends FeatureProcessor {
100033
100251
  if (reportedDirs.some((dir) => isAncestorOrSelf(dir, file.dir))) continue;
100034
100252
  const chain = files.filter(({ dir }) => isAncestorOrSelf(dir, file.dir));
100035
100253
  const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
100036
- if (chainBytes > budget.bytes) {
100254
+ if (chainBytes > limitBytes) {
100037
100255
  reportedDirs.push(file.dir);
100038
- this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${budget.bytes}-byte limit (as is every directory below it). ${budget.remedy}`);
100256
+ this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${limitLabel} (as is every directory below it). ${budget.remedy}`);
100039
100257
  }
100040
100258
  }
100041
100259
  }