rulesync 8.15.1 → 8.17.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.
@@ -4826,7 +4826,9 @@ async function buildCodexConfigTomlContent({
4826
4826
  if (typeof configToml.features !== "object" || configToml.features === null) {
4827
4827
  configToml.features = {};
4828
4828
  }
4829
- configToml.features.codex_hooks = true;
4829
+ const features = configToml.features;
4830
+ delete features.codex_hooks;
4831
+ features.hooks = true;
4830
4832
  return smolToml2.stringify(configToml);
4831
4833
  }
4832
4834
  var CodexcliConfigToml = class _CodexcliConfigToml extends ToolFile {
@@ -7608,6 +7610,16 @@ var McpServerSchema = z24.looseObject({
7608
7610
  tools: z24.optional(z24.array(z24.string())),
7609
7611
  kiroAutoApprove: z24.optional(z24.array(z24.string())),
7610
7612
  kiroAutoBlock: z24.optional(z24.array(z24.string())),
7613
+ // Codex CLI-specific: list of shell env var names that codex should pass
7614
+ // through from the user's environment to the MCP server process.
7615
+ // Distinct from `env` (a literal name→value map): `envVars` is a list of
7616
+ // variable NAMES whose values come from the user's shell at runtime.
7617
+ // Only honoured by the codex generator (renamed to `env_vars` in codex
7618
+ // TOML output, matching codex's native field name — see the
7619
+ // `enabledTools`→`enabled_tools` precedent in `codexcli-mcp.ts`).
7620
+ // Stripped by `RulesyncMcp.getMcpServers()` so it does not leak into
7621
+ // other tools' configs.
7622
+ envVars: z24.optional(z24.array(z24.string())),
7611
7623
  headers: z24.optional(z24.record(z24.string(), z24.string())),
7612
7624
  enabledTools: z24.optional(z24.array(z24.string())),
7613
7625
  disabledTools: z24.optional(z24.array(z24.string()))
@@ -7710,10 +7722,11 @@ var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
7710
7722
  });
7711
7723
  }
7712
7724
  getMcpServers() {
7713
- const entries = Object.entries(this.json.mcpServers);
7725
+ const mcpServers = this.json.mcpServers ?? {};
7726
+ const entries = Object.entries(mcpServers);
7714
7727
  return Object.fromEntries(
7715
7728
  entries.map(([serverName, serverConfig]) => {
7716
- return [serverName, omit(serverConfig, ["targets", "description", "exposed"])];
7729
+ return [serverName, omit(serverConfig, ["targets", "description", "exposed", "envVars"])];
7717
7730
  })
7718
7731
  );
7719
7732
  }
@@ -7932,11 +7945,17 @@ var ClineMcp = class _ClineMcp extends ToolMcp {
7932
7945
  rulesyncMcp,
7933
7946
  validate = true
7934
7947
  }) {
7948
+ const json = rulesyncMcp.getJson();
7949
+ const fileContent = JSON.stringify(
7950
+ { ...json, mcpServers: rulesyncMcp.getMcpServers() },
7951
+ null,
7952
+ 2
7953
+ );
7935
7954
  return new _ClineMcp({
7936
7955
  outputRoot,
7937
7956
  relativeDirPath: this.getSettablePaths().relativeDirPath,
7938
7957
  relativeFilePath: this.getSettablePaths().relativeFilePath,
7939
- fileContent: rulesyncMcp.getFileContent(),
7958
+ fileContent,
7940
7959
  validate
7941
7960
  });
7942
7961
  }
@@ -7980,6 +7999,8 @@ function convertFromCodexFormat(codexMcp) {
7980
7999
  converted["enabledTools"] = value;
7981
8000
  } else if (key === "disabled_tools") {
7982
8001
  converted["disabledTools"] = value;
8002
+ } else if (key === "env_vars") {
8003
+ converted["envVars"] = value;
7983
8004
  } else {
7984
8005
  converted[key] = value;
7985
8006
  }
@@ -8001,6 +8022,8 @@ function convertToCodexFormat(mcpServers) {
8001
8022
  converted["enabled_tools"] = value;
8002
8023
  } else if (key === "disabledTools") {
8003
8024
  converted["disabled_tools"] = value;
8025
+ } else if (key === "envVars") {
8026
+ converted["env_vars"] = value;
8004
8027
  } else {
8005
8028
  converted[key] = value;
8006
8029
  }
@@ -8096,7 +8119,12 @@ var CodexcliMcp = class _CodexcliMcp extends ToolMcp {
8096
8119
  const filtered = {};
8097
8120
  for (const [key, value] of Object.entries(obj)) {
8098
8121
  if (value === null) continue;
8099
- if (typeof value === "object" && Object.keys(value).length === 0) continue;
8122
+ if (typeof value === "object" && !Array.isArray(value)) {
8123
+ const cleaned = this.removeEmptyEntries(value);
8124
+ if (Object.keys(cleaned).length === 0) continue;
8125
+ filtered[key] = cleaned;
8126
+ continue;
8127
+ }
8100
8128
  filtered[key] = value;
8101
8129
  }
8102
8130
  return filtered;
@@ -8473,8 +8501,7 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
8473
8501
  { cause: error }
8474
8502
  );
8475
8503
  }
8476
- const rulesyncJson = rulesyncMcp.getJson();
8477
- const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
8504
+ const mcpServers = rulesyncMcp.getMcpServers();
8478
8505
  const transformedServers = convertEnvToCursorFormat(mcpServers);
8479
8506
  const cursorConfig = { ...json, mcpServers: transformedServers };
8480
8507
  return new _CursorMcp({
@@ -8643,9 +8670,9 @@ var FactorydroidMcp = class _FactorydroidMcp extends ToolMcp {
8643
8670
  rulesyncMcp,
8644
8671
  validate = true
8645
8672
  }) {
8646
- const json = rulesyncMcp.getJson();
8673
+ const mcpServers = rulesyncMcp.getMcpServers();
8647
8674
  const factorydroidConfig = {
8648
- mcpServers: json.mcpServers || {}
8675
+ mcpServers
8649
8676
  };
8650
8677
  const fileContent = JSON.stringify(factorydroidConfig, null, 2);
8651
8678
  return new _FactorydroidMcp({
@@ -8731,7 +8758,7 @@ var GeminiCliMcp = class _GeminiCliMcp extends ToolMcp {
8731
8758
  JSON.stringify({ mcpServers: {} }, null, 2)
8732
8759
  );
8733
8760
  const json = JSON.parse(fileContent);
8734
- const newJson = { ...json, mcpServers: rulesyncMcp.getJson().mcpServers };
8761
+ const newJson = { ...json, mcpServers: rulesyncMcp.getMcpServers() };
8735
8762
  return new _GeminiCliMcp({
8736
8763
  outputRoot,
8737
8764
  relativeDirPath: paths.relativeDirPath,
@@ -8812,11 +8839,17 @@ var JunieMcp = class _JunieMcp extends ToolMcp {
8812
8839
  rulesyncMcp,
8813
8840
  validate = true
8814
8841
  }) {
8842
+ const json = rulesyncMcp.getJson();
8843
+ const fileContent = JSON.stringify(
8844
+ { ...json, mcpServers: rulesyncMcp.getMcpServers() },
8845
+ null,
8846
+ 2
8847
+ );
8815
8848
  return new _JunieMcp({
8816
8849
  outputRoot,
8817
8850
  relativeDirPath: this.getSettablePaths().relativeDirPath,
8818
8851
  relativeFilePath: this.getSettablePaths().relativeFilePath,
8819
- fileContent: rulesyncMcp.getFileContent(),
8852
+ fileContent,
8820
8853
  validate
8821
8854
  });
8822
8855
  }
@@ -9596,8 +9629,7 @@ var RovodevMcp = class _RovodevMcp extends ToolMcp {
9596
9629
  JSON.stringify({ mcpServers: {} }, null, 2)
9597
9630
  );
9598
9631
  const json = parseRovodevMcpJson(fileContent, paths.relativeDirPath, paths.relativeFilePath);
9599
- const rulesyncJson = rulesyncMcp.getJson();
9600
- const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
9632
+ const mcpServers = rulesyncMcp.getMcpServers();
9601
9633
  const rovodevConfig = { ...json, mcpServers };
9602
9634
  return new _RovodevMcp({
9603
9635
  outputRoot,
@@ -10897,10 +10929,12 @@ var ClinePermissions = class _ClinePermissions extends ToolPermissions {
10897
10929
  };
10898
10930
 
10899
10931
  // src/features/permissions/codexcli-permissions.ts
10900
- import { join as join68 } from "path";
10932
+ import { isAbsolute as isAbsolute3, join as join68 } from "path";
10901
10933
  import * as smolToml4 from "smol-toml";
10902
10934
  var RULESYNC_PROFILE_NAME = "rulesync";
10903
10935
  var RULESYNC_BASH_RULES_FILE_NAME = "rulesync.rules";
10936
+ var CODEX_PROJECT_ROOTS_KEY = ":project_roots";
10937
+ var CODEX_GLOB_SCAN_MAX_DEPTH = 8;
10904
10938
  var CodexcliPermissions = class _CodexcliPermissions extends ToolPermissions {
10905
10939
  static getSettablePaths(_options = {}) {
10906
10940
  return {
@@ -11011,17 +11045,30 @@ function convertRulesyncToCodexProfile({
11011
11045
  logger
11012
11046
  }) {
11013
11047
  const filesystem = {};
11048
+ const projectRootFilesystem = {};
11014
11049
  const domains = {};
11015
11050
  for (const [toolName, rules] of Object.entries(config.permission)) {
11016
11051
  if (toolName === "read") {
11017
11052
  for (const [pattern, action] of Object.entries(rules)) {
11018
- filesystem[pattern] = mapReadAction(action);
11053
+ addFilesystemRule({
11054
+ filesystem,
11055
+ projectRootFilesystem,
11056
+ pattern,
11057
+ access: mapReadAction(action),
11058
+ logger
11059
+ });
11019
11060
  }
11020
11061
  continue;
11021
11062
  }
11022
11063
  if (toolName === "edit" || toolName === "write") {
11023
11064
  for (const [pattern, action] of Object.entries(rules)) {
11024
- filesystem[pattern] = mapWriteAction(action);
11065
+ addFilesystemRule({
11066
+ filesystem,
11067
+ projectRootFilesystem,
11068
+ pattern,
11069
+ access: mapWriteAction(action),
11070
+ logger
11071
+ });
11025
11072
  }
11026
11073
  continue;
11027
11074
  }
@@ -11041,6 +11088,17 @@ function convertRulesyncToCodexProfile({
11041
11088
  `Codex CLI permissions support only read/edit/write/webfetch categories. Skipping: ${toolName}`
11042
11089
  );
11043
11090
  }
11091
+ if (Object.keys(projectRootFilesystem).length > 0) {
11092
+ if (typeof filesystem[CODEX_PROJECT_ROOTS_KEY] === "string") {
11093
+ logger?.warn(
11094
+ `"${CODEX_PROJECT_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions, but it will be overwritten by project-root rules. Consider removing the direct "${CODEX_PROJECT_ROOTS_KEY}" entry.`
11095
+ );
11096
+ }
11097
+ if (Object.keys(projectRootFilesystem).some((pattern) => pattern.includes("**"))) {
11098
+ filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
11099
+ }
11100
+ filesystem[CODEX_PROJECT_ROOTS_KEY] = projectRootFilesystem;
11101
+ }
11044
11102
  return {
11045
11103
  ...Object.keys(filesystem).length > 0 ? { filesystem } : {},
11046
11104
  ...Object.keys(domains).length > 0 ? { network: { domains } } : {}
@@ -11052,13 +11110,14 @@ function convertCodexProfileToRulesync(profile) {
11052
11110
  permission.read = {};
11053
11111
  permission.edit = {};
11054
11112
  for (const [pattern, access] of Object.entries(profile.filesystem)) {
11055
- if (access === "none") {
11056
- permission.read[pattern] = "deny";
11057
- permission.edit[pattern] = "deny";
11058
- } else if (access === "read") {
11059
- permission.read[pattern] = "allow";
11060
- } else {
11061
- permission.edit[pattern] = "allow";
11113
+ if (isCodexFilesystemAccess(access)) {
11114
+ addRulesyncFilesystemRule(permission, pattern, access);
11115
+ continue;
11116
+ }
11117
+ if (isCodexFilesystemRuleTable(access)) {
11118
+ for (const [nestedPattern, nestedAccess] of Object.entries(access)) {
11119
+ addRulesyncFilesystemRule(permission, nestedPattern, nestedAccess);
11120
+ }
11062
11121
  }
11063
11122
  }
11064
11123
  }
@@ -11081,6 +11140,40 @@ function toCodexProfile(value) {
11081
11140
  ...domains ? { network: { domains } } : {}
11082
11141
  };
11083
11142
  }
11143
+ function addFilesystemRule({
11144
+ filesystem,
11145
+ projectRootFilesystem,
11146
+ pattern,
11147
+ access,
11148
+ logger
11149
+ }) {
11150
+ if (pattern.trim() === "") {
11151
+ logger?.warn("Skipping empty pattern in filesystem permissions.");
11152
+ return;
11153
+ }
11154
+ if (canBeCodexFilesystemRoot(pattern)) {
11155
+ filesystem[pattern] = access;
11156
+ return;
11157
+ }
11158
+ projectRootFilesystem[pattern] = access;
11159
+ }
11160
+ function canBeCodexFilesystemRoot(pattern) {
11161
+ return isAbsolute3(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith("~/") || pattern === "~" || pattern.startsWith(":");
11162
+ }
11163
+ function addRulesyncFilesystemRule(permission, pattern, access) {
11164
+ if (access === "none") {
11165
+ permission.read ??= {};
11166
+ permission.edit ??= {};
11167
+ permission.read[pattern] = "deny";
11168
+ permission.edit[pattern] = "deny";
11169
+ } else if (access === "read") {
11170
+ permission.read ??= {};
11171
+ permission.read[pattern] = "allow";
11172
+ } else {
11173
+ permission.edit ??= {};
11174
+ permission.edit[pattern] = "allow";
11175
+ }
11176
+ }
11084
11177
  function toMutableTable(value) {
11085
11178
  if (!value || typeof value !== "object" || Array.isArray(value)) {
11086
11179
  return {};
@@ -11091,8 +11184,33 @@ function toFilesystemRecord(value) {
11091
11184
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
11092
11185
  const result = {};
11093
11186
  for (const [key, raw] of Object.entries(value)) {
11094
- if (typeof raw !== "string") continue;
11095
- if (raw === "read" || raw === "write" || raw === "none") {
11187
+ if (isCodexFilesystemAccess(raw)) {
11188
+ result[key] = raw;
11189
+ continue;
11190
+ }
11191
+ if (key === "glob_scan_max_depth" && typeof raw === "number") {
11192
+ result[key] = raw;
11193
+ continue;
11194
+ }
11195
+ const nested = toCodexFilesystemRuleTable(raw);
11196
+ if (nested) {
11197
+ result[key] = nested;
11198
+ }
11199
+ }
11200
+ return Object.keys(result).length > 0 ? result : void 0;
11201
+ }
11202
+ function isCodexFilesystemAccess(value) {
11203
+ return value === "read" || value === "write" || value === "none";
11204
+ }
11205
+ function isCodexFilesystemRuleTable(value) {
11206
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
11207
+ return Object.values(value).every(isCodexFilesystemAccess);
11208
+ }
11209
+ function toCodexFilesystemRuleTable(value) {
11210
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
11211
+ const result = {};
11212
+ for (const [key, raw] of Object.entries(value)) {
11213
+ if (isCodexFilesystemAccess(raw)) {
11096
11214
  result[key] = raw;
11097
11215
  }
11098
11216
  }
@@ -13306,7 +13424,8 @@ var RulesyncSkillFrontmatterSchemaInternal = z39.looseObject({
13306
13424
  "allowed-tools": z39.optional(z39.array(z39.string())),
13307
13425
  model: z39.optional(z39.string()),
13308
13426
  "disable-model-invocation": z39.optional(z39.boolean()),
13309
- "scheduled-task": z39.optional(z39.boolean())
13427
+ "scheduled-task": z39.optional(z39.boolean()),
13428
+ paths: z39.optional(z39.union([z39.string(), z39.array(z39.string())]))
13310
13429
  })
13311
13430
  ),
13312
13431
  codexcli: z39.optional(
@@ -14081,7 +14200,8 @@ var ClaudecodeSkillFrontmatterSchema = z43.looseObject({
14081
14200
  description: z43.string(),
14082
14201
  "allowed-tools": z43.optional(z43.array(z43.string())),
14083
14202
  model: z43.optional(z43.string()),
14084
- "disable-model-invocation": z43.optional(z43.boolean())
14203
+ "disable-model-invocation": z43.optional(z43.boolean()),
14204
+ paths: z43.optional(z43.union([z43.string(), z43.array(z43.string())]))
14085
14205
  });
14086
14206
  var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14087
14207
  constructor({
@@ -14154,6 +14274,7 @@ var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14154
14274
  ...frontmatter["disable-model-invocation"] !== void 0 && {
14155
14275
  "disable-model-invocation": frontmatter["disable-model-invocation"]
14156
14276
  },
14277
+ ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
14157
14278
  ...this.relativeDirPath === CLAUDE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true }
14158
14279
  };
14159
14280
  const rulesyncFrontmatter = {
@@ -14191,6 +14312,9 @@ var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14191
14312
  },
14192
14313
  ...rulesyncFrontmatter.claudecode?.["disable-model-invocation"] !== void 0 && {
14193
14314
  "disable-model-invocation": rulesyncFrontmatter.claudecode["disable-model-invocation"]
14315
+ },
14316
+ ...rulesyncFrontmatter.claudecode?.paths !== void 0 && {
14317
+ paths: rulesyncFrontmatter.claudecode.paths
14194
14318
  }
14195
14319
  };
14196
14320
  const settablePaths = _ClaudecodeSkill.getSettablePaths({ global });
@@ -4859,7 +4859,9 @@ async function buildCodexConfigTomlContent({
4859
4859
  if (typeof configToml.features !== "object" || configToml.features === null) {
4860
4860
  configToml.features = {};
4861
4861
  }
4862
- configToml.features.codex_hooks = true;
4862
+ const features = configToml.features;
4863
+ delete features.codex_hooks;
4864
+ features.hooks = true;
4863
4865
  return smolToml2.stringify(configToml);
4864
4866
  }
4865
4867
  var CodexcliConfigToml = class _CodexcliConfigToml extends ToolFile {
@@ -7641,6 +7643,16 @@ var McpServerSchema = import_mini24.z.looseObject({
7641
7643
  tools: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7642
7644
  kiroAutoApprove: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7643
7645
  kiroAutoBlock: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7646
+ // Codex CLI-specific: list of shell env var names that codex should pass
7647
+ // through from the user's environment to the MCP server process.
7648
+ // Distinct from `env` (a literal name→value map): `envVars` is a list of
7649
+ // variable NAMES whose values come from the user's shell at runtime.
7650
+ // Only honoured by the codex generator (renamed to `env_vars` in codex
7651
+ // TOML output, matching codex's native field name — see the
7652
+ // `enabledTools`→`enabled_tools` precedent in `codexcli-mcp.ts`).
7653
+ // Stripped by `RulesyncMcp.getMcpServers()` so it does not leak into
7654
+ // other tools' configs.
7655
+ envVars: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7644
7656
  headers: import_mini24.z.optional(import_mini24.z.record(import_mini24.z.string(), import_mini24.z.string())),
7645
7657
  enabledTools: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7646
7658
  disabledTools: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string()))
@@ -7743,10 +7755,11 @@ var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
7743
7755
  });
7744
7756
  }
7745
7757
  getMcpServers() {
7746
- const entries = Object.entries(this.json.mcpServers);
7758
+ const mcpServers = this.json.mcpServers ?? {};
7759
+ const entries = Object.entries(mcpServers);
7747
7760
  return Object.fromEntries(
7748
7761
  entries.map(([serverName, serverConfig]) => {
7749
- return [serverName, (0, import_object.omit)(serverConfig, ["targets", "description", "exposed"])];
7762
+ return [serverName, (0, import_object.omit)(serverConfig, ["targets", "description", "exposed", "envVars"])];
7750
7763
  })
7751
7764
  );
7752
7765
  }
@@ -7965,11 +7978,17 @@ var ClineMcp = class _ClineMcp extends ToolMcp {
7965
7978
  rulesyncMcp,
7966
7979
  validate = true
7967
7980
  }) {
7981
+ const json = rulesyncMcp.getJson();
7982
+ const fileContent = JSON.stringify(
7983
+ { ...json, mcpServers: rulesyncMcp.getMcpServers() },
7984
+ null,
7985
+ 2
7986
+ );
7968
7987
  return new _ClineMcp({
7969
7988
  outputRoot,
7970
7989
  relativeDirPath: this.getSettablePaths().relativeDirPath,
7971
7990
  relativeFilePath: this.getSettablePaths().relativeFilePath,
7972
- fileContent: rulesyncMcp.getFileContent(),
7991
+ fileContent,
7973
7992
  validate
7974
7993
  });
7975
7994
  }
@@ -8013,6 +8032,8 @@ function convertFromCodexFormat(codexMcp) {
8013
8032
  converted["enabledTools"] = value;
8014
8033
  } else if (key === "disabled_tools") {
8015
8034
  converted["disabledTools"] = value;
8035
+ } else if (key === "env_vars") {
8036
+ converted["envVars"] = value;
8016
8037
  } else {
8017
8038
  converted[key] = value;
8018
8039
  }
@@ -8034,6 +8055,8 @@ function convertToCodexFormat(mcpServers) {
8034
8055
  converted["enabled_tools"] = value;
8035
8056
  } else if (key === "disabledTools") {
8036
8057
  converted["disabled_tools"] = value;
8058
+ } else if (key === "envVars") {
8059
+ converted["env_vars"] = value;
8037
8060
  } else {
8038
8061
  converted[key] = value;
8039
8062
  }
@@ -8129,7 +8152,12 @@ var CodexcliMcp = class _CodexcliMcp extends ToolMcp {
8129
8152
  const filtered = {};
8130
8153
  for (const [key, value] of Object.entries(obj)) {
8131
8154
  if (value === null) continue;
8132
- if (typeof value === "object" && Object.keys(value).length === 0) continue;
8155
+ if (typeof value === "object" && !Array.isArray(value)) {
8156
+ const cleaned = this.removeEmptyEntries(value);
8157
+ if (Object.keys(cleaned).length === 0) continue;
8158
+ filtered[key] = cleaned;
8159
+ continue;
8160
+ }
8133
8161
  filtered[key] = value;
8134
8162
  }
8135
8163
  return filtered;
@@ -8506,8 +8534,7 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
8506
8534
  { cause: error }
8507
8535
  );
8508
8536
  }
8509
- const rulesyncJson = rulesyncMcp.getJson();
8510
- const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
8537
+ const mcpServers = rulesyncMcp.getMcpServers();
8511
8538
  const transformedServers = convertEnvToCursorFormat(mcpServers);
8512
8539
  const cursorConfig = { ...json, mcpServers: transformedServers };
8513
8540
  return new _CursorMcp({
@@ -8676,9 +8703,9 @@ var FactorydroidMcp = class _FactorydroidMcp extends ToolMcp {
8676
8703
  rulesyncMcp,
8677
8704
  validate = true
8678
8705
  }) {
8679
- const json = rulesyncMcp.getJson();
8706
+ const mcpServers = rulesyncMcp.getMcpServers();
8680
8707
  const factorydroidConfig = {
8681
- mcpServers: json.mcpServers || {}
8708
+ mcpServers
8682
8709
  };
8683
8710
  const fileContent = JSON.stringify(factorydroidConfig, null, 2);
8684
8711
  return new _FactorydroidMcp({
@@ -8764,7 +8791,7 @@ var GeminiCliMcp = class _GeminiCliMcp extends ToolMcp {
8764
8791
  JSON.stringify({ mcpServers: {} }, null, 2)
8765
8792
  );
8766
8793
  const json = JSON.parse(fileContent);
8767
- const newJson = { ...json, mcpServers: rulesyncMcp.getJson().mcpServers };
8794
+ const newJson = { ...json, mcpServers: rulesyncMcp.getMcpServers() };
8768
8795
  return new _GeminiCliMcp({
8769
8796
  outputRoot,
8770
8797
  relativeDirPath: paths.relativeDirPath,
@@ -8845,11 +8872,17 @@ var JunieMcp = class _JunieMcp extends ToolMcp {
8845
8872
  rulesyncMcp,
8846
8873
  validate = true
8847
8874
  }) {
8875
+ const json = rulesyncMcp.getJson();
8876
+ const fileContent = JSON.stringify(
8877
+ { ...json, mcpServers: rulesyncMcp.getMcpServers() },
8878
+ null,
8879
+ 2
8880
+ );
8848
8881
  return new _JunieMcp({
8849
8882
  outputRoot,
8850
8883
  relativeDirPath: this.getSettablePaths().relativeDirPath,
8851
8884
  relativeFilePath: this.getSettablePaths().relativeFilePath,
8852
- fileContent: rulesyncMcp.getFileContent(),
8885
+ fileContent,
8853
8886
  validate
8854
8887
  });
8855
8888
  }
@@ -9629,8 +9662,7 @@ var RovodevMcp = class _RovodevMcp extends ToolMcp {
9629
9662
  JSON.stringify({ mcpServers: {} }, null, 2)
9630
9663
  );
9631
9664
  const json = parseRovodevMcpJson(fileContent, paths.relativeDirPath, paths.relativeFilePath);
9632
- const rulesyncJson = rulesyncMcp.getJson();
9633
- const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
9665
+ const mcpServers = rulesyncMcp.getMcpServers();
9634
9666
  const rovodevConfig = { ...json, mcpServers };
9635
9667
  return new _RovodevMcp({
9636
9668
  outputRoot,
@@ -10934,6 +10966,8 @@ var import_node_path71 = require("path");
10934
10966
  var smolToml4 = __toESM(require("smol-toml"), 1);
10935
10967
  var RULESYNC_PROFILE_NAME = "rulesync";
10936
10968
  var RULESYNC_BASH_RULES_FILE_NAME = "rulesync.rules";
10969
+ var CODEX_PROJECT_ROOTS_KEY = ":project_roots";
10970
+ var CODEX_GLOB_SCAN_MAX_DEPTH = 8;
10937
10971
  var CodexcliPermissions = class _CodexcliPermissions extends ToolPermissions {
10938
10972
  static getSettablePaths(_options = {}) {
10939
10973
  return {
@@ -11044,17 +11078,30 @@ function convertRulesyncToCodexProfile({
11044
11078
  logger: logger5
11045
11079
  }) {
11046
11080
  const filesystem = {};
11081
+ const projectRootFilesystem = {};
11047
11082
  const domains = {};
11048
11083
  for (const [toolName, rules] of Object.entries(config.permission)) {
11049
11084
  if (toolName === "read") {
11050
11085
  for (const [pattern, action] of Object.entries(rules)) {
11051
- filesystem[pattern] = mapReadAction(action);
11086
+ addFilesystemRule({
11087
+ filesystem,
11088
+ projectRootFilesystem,
11089
+ pattern,
11090
+ access: mapReadAction(action),
11091
+ logger: logger5
11092
+ });
11052
11093
  }
11053
11094
  continue;
11054
11095
  }
11055
11096
  if (toolName === "edit" || toolName === "write") {
11056
11097
  for (const [pattern, action] of Object.entries(rules)) {
11057
- filesystem[pattern] = mapWriteAction(action);
11098
+ addFilesystemRule({
11099
+ filesystem,
11100
+ projectRootFilesystem,
11101
+ pattern,
11102
+ access: mapWriteAction(action),
11103
+ logger: logger5
11104
+ });
11058
11105
  }
11059
11106
  continue;
11060
11107
  }
@@ -11074,6 +11121,17 @@ function convertRulesyncToCodexProfile({
11074
11121
  `Codex CLI permissions support only read/edit/write/webfetch categories. Skipping: ${toolName}`
11075
11122
  );
11076
11123
  }
11124
+ if (Object.keys(projectRootFilesystem).length > 0) {
11125
+ if (typeof filesystem[CODEX_PROJECT_ROOTS_KEY] === "string") {
11126
+ logger5?.warn(
11127
+ `"${CODEX_PROJECT_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions, but it will be overwritten by project-root rules. Consider removing the direct "${CODEX_PROJECT_ROOTS_KEY}" entry.`
11128
+ );
11129
+ }
11130
+ if (Object.keys(projectRootFilesystem).some((pattern) => pattern.includes("**"))) {
11131
+ filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
11132
+ }
11133
+ filesystem[CODEX_PROJECT_ROOTS_KEY] = projectRootFilesystem;
11134
+ }
11077
11135
  return {
11078
11136
  ...Object.keys(filesystem).length > 0 ? { filesystem } : {},
11079
11137
  ...Object.keys(domains).length > 0 ? { network: { domains } } : {}
@@ -11085,13 +11143,14 @@ function convertCodexProfileToRulesync(profile) {
11085
11143
  permission.read = {};
11086
11144
  permission.edit = {};
11087
11145
  for (const [pattern, access] of Object.entries(profile.filesystem)) {
11088
- if (access === "none") {
11089
- permission.read[pattern] = "deny";
11090
- permission.edit[pattern] = "deny";
11091
- } else if (access === "read") {
11092
- permission.read[pattern] = "allow";
11093
- } else {
11094
- permission.edit[pattern] = "allow";
11146
+ if (isCodexFilesystemAccess(access)) {
11147
+ addRulesyncFilesystemRule(permission, pattern, access);
11148
+ continue;
11149
+ }
11150
+ if (isCodexFilesystemRuleTable(access)) {
11151
+ for (const [nestedPattern, nestedAccess] of Object.entries(access)) {
11152
+ addRulesyncFilesystemRule(permission, nestedPattern, nestedAccess);
11153
+ }
11095
11154
  }
11096
11155
  }
11097
11156
  }
@@ -11114,6 +11173,40 @@ function toCodexProfile(value) {
11114
11173
  ...domains ? { network: { domains } } : {}
11115
11174
  };
11116
11175
  }
11176
+ function addFilesystemRule({
11177
+ filesystem,
11178
+ projectRootFilesystem,
11179
+ pattern,
11180
+ access,
11181
+ logger: logger5
11182
+ }) {
11183
+ if (pattern.trim() === "") {
11184
+ logger5?.warn("Skipping empty pattern in filesystem permissions.");
11185
+ return;
11186
+ }
11187
+ if (canBeCodexFilesystemRoot(pattern)) {
11188
+ filesystem[pattern] = access;
11189
+ return;
11190
+ }
11191
+ projectRootFilesystem[pattern] = access;
11192
+ }
11193
+ function canBeCodexFilesystemRoot(pattern) {
11194
+ return (0, import_node_path71.isAbsolute)(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith("~/") || pattern === "~" || pattern.startsWith(":");
11195
+ }
11196
+ function addRulesyncFilesystemRule(permission, pattern, access) {
11197
+ if (access === "none") {
11198
+ permission.read ??= {};
11199
+ permission.edit ??= {};
11200
+ permission.read[pattern] = "deny";
11201
+ permission.edit[pattern] = "deny";
11202
+ } else if (access === "read") {
11203
+ permission.read ??= {};
11204
+ permission.read[pattern] = "allow";
11205
+ } else {
11206
+ permission.edit ??= {};
11207
+ permission.edit[pattern] = "allow";
11208
+ }
11209
+ }
11117
11210
  function toMutableTable(value) {
11118
11211
  if (!value || typeof value !== "object" || Array.isArray(value)) {
11119
11212
  return {};
@@ -11124,8 +11217,33 @@ function toFilesystemRecord(value) {
11124
11217
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
11125
11218
  const result = {};
11126
11219
  for (const [key, raw] of Object.entries(value)) {
11127
- if (typeof raw !== "string") continue;
11128
- if (raw === "read" || raw === "write" || raw === "none") {
11220
+ if (isCodexFilesystemAccess(raw)) {
11221
+ result[key] = raw;
11222
+ continue;
11223
+ }
11224
+ if (key === "glob_scan_max_depth" && typeof raw === "number") {
11225
+ result[key] = raw;
11226
+ continue;
11227
+ }
11228
+ const nested = toCodexFilesystemRuleTable(raw);
11229
+ if (nested) {
11230
+ result[key] = nested;
11231
+ }
11232
+ }
11233
+ return Object.keys(result).length > 0 ? result : void 0;
11234
+ }
11235
+ function isCodexFilesystemAccess(value) {
11236
+ return value === "read" || value === "write" || value === "none";
11237
+ }
11238
+ function isCodexFilesystemRuleTable(value) {
11239
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
11240
+ return Object.values(value).every(isCodexFilesystemAccess);
11241
+ }
11242
+ function toCodexFilesystemRuleTable(value) {
11243
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
11244
+ const result = {};
11245
+ for (const [key, raw] of Object.entries(value)) {
11246
+ if (isCodexFilesystemAccess(raw)) {
11129
11247
  result[key] = raw;
11130
11248
  }
11131
11249
  }
@@ -13339,7 +13457,8 @@ var RulesyncSkillFrontmatterSchemaInternal = import_mini39.z.looseObject({
13339
13457
  "allowed-tools": import_mini39.z.optional(import_mini39.z.array(import_mini39.z.string())),
13340
13458
  model: import_mini39.z.optional(import_mini39.z.string()),
13341
13459
  "disable-model-invocation": import_mini39.z.optional(import_mini39.z.boolean()),
13342
- "scheduled-task": import_mini39.z.optional(import_mini39.z.boolean())
13460
+ "scheduled-task": import_mini39.z.optional(import_mini39.z.boolean()),
13461
+ paths: import_mini39.z.optional(import_mini39.z.union([import_mini39.z.string(), import_mini39.z.array(import_mini39.z.string())]))
13343
13462
  })
13344
13463
  ),
13345
13464
  codexcli: import_mini39.z.optional(
@@ -14114,7 +14233,8 @@ var ClaudecodeSkillFrontmatterSchema = import_mini43.z.looseObject({
14114
14233
  description: import_mini43.z.string(),
14115
14234
  "allowed-tools": import_mini43.z.optional(import_mini43.z.array(import_mini43.z.string())),
14116
14235
  model: import_mini43.z.optional(import_mini43.z.string()),
14117
- "disable-model-invocation": import_mini43.z.optional(import_mini43.z.boolean())
14236
+ "disable-model-invocation": import_mini43.z.optional(import_mini43.z.boolean()),
14237
+ paths: import_mini43.z.optional(import_mini43.z.union([import_mini43.z.string(), import_mini43.z.array(import_mini43.z.string())]))
14118
14238
  });
14119
14239
  var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14120
14240
  constructor({
@@ -14187,6 +14307,7 @@ var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14187
14307
  ...frontmatter["disable-model-invocation"] !== void 0 && {
14188
14308
  "disable-model-invocation": frontmatter["disable-model-invocation"]
14189
14309
  },
14310
+ ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
14190
14311
  ...this.relativeDirPath === CLAUDE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true }
14191
14312
  };
14192
14313
  const rulesyncFrontmatter = {
@@ -14224,6 +14345,9 @@ var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14224
14345
  },
14225
14346
  ...rulesyncFrontmatter.claudecode?.["disable-model-invocation"] !== void 0 && {
14226
14347
  "disable-model-invocation": rulesyncFrontmatter.claudecode["disable-model-invocation"]
14348
+ },
14349
+ ...rulesyncFrontmatter.claudecode?.paths !== void 0 && {
14350
+ paths: rulesyncFrontmatter.claudecode.paths
14227
14351
  }
14228
14352
  };
14229
14353
  const settablePaths = _ClaudecodeSkill.getSettablePaths({ global });
@@ -31629,7 +31753,7 @@ function wrapCommand({
31629
31753
  }
31630
31754
 
31631
31755
  // src/cli/index.ts
31632
- var getVersion = () => "8.15.1";
31756
+ var getVersion = () => "8.17.0";
31633
31757
  function wrapCommand2(name, errorCode, handler) {
31634
31758
  return wrapCommand({ name, errorCode, handler, getVersion });
31635
31759
  }
package/dist/cli/index.js CHANGED
@@ -79,7 +79,7 @@ import {
79
79
  stringifyFrontmatter,
80
80
  toPosixPath,
81
81
  writeFileContent
82
- } from "../chunk-3DM6NLND.js";
82
+ } from "../chunk-RXCRO26Z.js";
83
83
 
84
84
  // src/cli/index.ts
85
85
  import { Command } from "commander";
@@ -6485,7 +6485,7 @@ function wrapCommand({
6485
6485
  }
6486
6486
 
6487
6487
  // src/cli/index.ts
6488
- var getVersion = () => "8.15.1";
6488
+ var getVersion = () => "8.17.0";
6489
6489
  function wrapCommand2(name, errorCode, handler) {
6490
6490
  return wrapCommand({ name, errorCode, handler, getVersion });
6491
6491
  }
package/dist/index.cjs CHANGED
@@ -4732,7 +4732,9 @@ async function buildCodexConfigTomlContent({
4732
4732
  if (typeof configToml.features !== "object" || configToml.features === null) {
4733
4733
  configToml.features = {};
4734
4734
  }
4735
- configToml.features.codex_hooks = true;
4735
+ const features = configToml.features;
4736
+ delete features.codex_hooks;
4737
+ features.hooks = true;
4736
4738
  return smolToml2.stringify(configToml);
4737
4739
  }
4738
4740
  var CodexcliConfigToml = class _CodexcliConfigToml extends ToolFile {
@@ -7514,6 +7516,16 @@ var McpServerSchema = import_mini24.z.looseObject({
7514
7516
  tools: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7515
7517
  kiroAutoApprove: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7516
7518
  kiroAutoBlock: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7519
+ // Codex CLI-specific: list of shell env var names that codex should pass
7520
+ // through from the user's environment to the MCP server process.
7521
+ // Distinct from `env` (a literal name→value map): `envVars` is a list of
7522
+ // variable NAMES whose values come from the user's shell at runtime.
7523
+ // Only honoured by the codex generator (renamed to `env_vars` in codex
7524
+ // TOML output, matching codex's native field name — see the
7525
+ // `enabledTools`→`enabled_tools` precedent in `codexcli-mcp.ts`).
7526
+ // Stripped by `RulesyncMcp.getMcpServers()` so it does not leak into
7527
+ // other tools' configs.
7528
+ envVars: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7517
7529
  headers: import_mini24.z.optional(import_mini24.z.record(import_mini24.z.string(), import_mini24.z.string())),
7518
7530
  enabledTools: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string())),
7519
7531
  disabledTools: import_mini24.z.optional(import_mini24.z.array(import_mini24.z.string()))
@@ -7616,10 +7628,11 @@ var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
7616
7628
  });
7617
7629
  }
7618
7630
  getMcpServers() {
7619
- const entries = Object.entries(this.json.mcpServers);
7631
+ const mcpServers = this.json.mcpServers ?? {};
7632
+ const entries = Object.entries(mcpServers);
7620
7633
  return Object.fromEntries(
7621
7634
  entries.map(([serverName, serverConfig]) => {
7622
- return [serverName, (0, import_object.omit)(serverConfig, ["targets", "description", "exposed"])];
7635
+ return [serverName, (0, import_object.omit)(serverConfig, ["targets", "description", "exposed", "envVars"])];
7623
7636
  })
7624
7637
  );
7625
7638
  }
@@ -7838,11 +7851,17 @@ var ClineMcp = class _ClineMcp extends ToolMcp {
7838
7851
  rulesyncMcp,
7839
7852
  validate = true
7840
7853
  }) {
7854
+ const json = rulesyncMcp.getJson();
7855
+ const fileContent = JSON.stringify(
7856
+ { ...json, mcpServers: rulesyncMcp.getMcpServers() },
7857
+ null,
7858
+ 2
7859
+ );
7841
7860
  return new _ClineMcp({
7842
7861
  outputRoot,
7843
7862
  relativeDirPath: this.getSettablePaths().relativeDirPath,
7844
7863
  relativeFilePath: this.getSettablePaths().relativeFilePath,
7845
- fileContent: rulesyncMcp.getFileContent(),
7864
+ fileContent,
7846
7865
  validate
7847
7866
  });
7848
7867
  }
@@ -7886,6 +7905,8 @@ function convertFromCodexFormat(codexMcp) {
7886
7905
  converted["enabledTools"] = value;
7887
7906
  } else if (key === "disabled_tools") {
7888
7907
  converted["disabledTools"] = value;
7908
+ } else if (key === "env_vars") {
7909
+ converted["envVars"] = value;
7889
7910
  } else {
7890
7911
  converted[key] = value;
7891
7912
  }
@@ -7907,6 +7928,8 @@ function convertToCodexFormat(mcpServers) {
7907
7928
  converted["enabled_tools"] = value;
7908
7929
  } else if (key === "disabledTools") {
7909
7930
  converted["disabled_tools"] = value;
7931
+ } else if (key === "envVars") {
7932
+ converted["env_vars"] = value;
7910
7933
  } else {
7911
7934
  converted[key] = value;
7912
7935
  }
@@ -8002,7 +8025,12 @@ var CodexcliMcp = class _CodexcliMcp extends ToolMcp {
8002
8025
  const filtered = {};
8003
8026
  for (const [key, value] of Object.entries(obj)) {
8004
8027
  if (value === null) continue;
8005
- if (typeof value === "object" && Object.keys(value).length === 0) continue;
8028
+ if (typeof value === "object" && !Array.isArray(value)) {
8029
+ const cleaned = this.removeEmptyEntries(value);
8030
+ if (Object.keys(cleaned).length === 0) continue;
8031
+ filtered[key] = cleaned;
8032
+ continue;
8033
+ }
8006
8034
  filtered[key] = value;
8007
8035
  }
8008
8036
  return filtered;
@@ -8379,8 +8407,7 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
8379
8407
  { cause: error }
8380
8408
  );
8381
8409
  }
8382
- const rulesyncJson = rulesyncMcp.getJson();
8383
- const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
8410
+ const mcpServers = rulesyncMcp.getMcpServers();
8384
8411
  const transformedServers = convertEnvToCursorFormat(mcpServers);
8385
8412
  const cursorConfig = { ...json, mcpServers: transformedServers };
8386
8413
  return new _CursorMcp({
@@ -8549,9 +8576,9 @@ var FactorydroidMcp = class _FactorydroidMcp extends ToolMcp {
8549
8576
  rulesyncMcp,
8550
8577
  validate = true
8551
8578
  }) {
8552
- const json = rulesyncMcp.getJson();
8579
+ const mcpServers = rulesyncMcp.getMcpServers();
8553
8580
  const factorydroidConfig = {
8554
- mcpServers: json.mcpServers || {}
8581
+ mcpServers
8555
8582
  };
8556
8583
  const fileContent = JSON.stringify(factorydroidConfig, null, 2);
8557
8584
  return new _FactorydroidMcp({
@@ -8637,7 +8664,7 @@ var GeminiCliMcp = class _GeminiCliMcp extends ToolMcp {
8637
8664
  JSON.stringify({ mcpServers: {} }, null, 2)
8638
8665
  );
8639
8666
  const json = JSON.parse(fileContent);
8640
- const newJson = { ...json, mcpServers: rulesyncMcp.getJson().mcpServers };
8667
+ const newJson = { ...json, mcpServers: rulesyncMcp.getMcpServers() };
8641
8668
  return new _GeminiCliMcp({
8642
8669
  outputRoot,
8643
8670
  relativeDirPath: paths.relativeDirPath,
@@ -8718,11 +8745,17 @@ var JunieMcp = class _JunieMcp extends ToolMcp {
8718
8745
  rulesyncMcp,
8719
8746
  validate = true
8720
8747
  }) {
8748
+ const json = rulesyncMcp.getJson();
8749
+ const fileContent = JSON.stringify(
8750
+ { ...json, mcpServers: rulesyncMcp.getMcpServers() },
8751
+ null,
8752
+ 2
8753
+ );
8721
8754
  return new _JunieMcp({
8722
8755
  outputRoot,
8723
8756
  relativeDirPath: this.getSettablePaths().relativeDirPath,
8724
8757
  relativeFilePath: this.getSettablePaths().relativeFilePath,
8725
- fileContent: rulesyncMcp.getFileContent(),
8758
+ fileContent,
8726
8759
  validate
8727
8760
  });
8728
8761
  }
@@ -9502,8 +9535,7 @@ var RovodevMcp = class _RovodevMcp extends ToolMcp {
9502
9535
  JSON.stringify({ mcpServers: {} }, null, 2)
9503
9536
  );
9504
9537
  const json = parseRovodevMcpJson(fileContent, paths.relativeDirPath, paths.relativeFilePath);
9505
- const rulesyncJson = rulesyncMcp.getJson();
9506
- const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
9538
+ const mcpServers = rulesyncMcp.getMcpServers();
9507
9539
  const rovodevConfig = { ...json, mcpServers };
9508
9540
  return new _RovodevMcp({
9509
9541
  outputRoot,
@@ -10807,6 +10839,8 @@ var import_node_path71 = require("path");
10807
10839
  var smolToml4 = __toESM(require("smol-toml"), 1);
10808
10840
  var RULESYNC_PROFILE_NAME = "rulesync";
10809
10841
  var RULESYNC_BASH_RULES_FILE_NAME = "rulesync.rules";
10842
+ var CODEX_PROJECT_ROOTS_KEY = ":project_roots";
10843
+ var CODEX_GLOB_SCAN_MAX_DEPTH = 8;
10810
10844
  var CodexcliPermissions = class _CodexcliPermissions extends ToolPermissions {
10811
10845
  static getSettablePaths(_options = {}) {
10812
10846
  return {
@@ -10917,17 +10951,30 @@ function convertRulesyncToCodexProfile({
10917
10951
  logger
10918
10952
  }) {
10919
10953
  const filesystem = {};
10954
+ const projectRootFilesystem = {};
10920
10955
  const domains = {};
10921
10956
  for (const [toolName, rules] of Object.entries(config.permission)) {
10922
10957
  if (toolName === "read") {
10923
10958
  for (const [pattern, action] of Object.entries(rules)) {
10924
- filesystem[pattern] = mapReadAction(action);
10959
+ addFilesystemRule({
10960
+ filesystem,
10961
+ projectRootFilesystem,
10962
+ pattern,
10963
+ access: mapReadAction(action),
10964
+ logger
10965
+ });
10925
10966
  }
10926
10967
  continue;
10927
10968
  }
10928
10969
  if (toolName === "edit" || toolName === "write") {
10929
10970
  for (const [pattern, action] of Object.entries(rules)) {
10930
- filesystem[pattern] = mapWriteAction(action);
10971
+ addFilesystemRule({
10972
+ filesystem,
10973
+ projectRootFilesystem,
10974
+ pattern,
10975
+ access: mapWriteAction(action),
10976
+ logger
10977
+ });
10931
10978
  }
10932
10979
  continue;
10933
10980
  }
@@ -10947,6 +10994,17 @@ function convertRulesyncToCodexProfile({
10947
10994
  `Codex CLI permissions support only read/edit/write/webfetch categories. Skipping: ${toolName}`
10948
10995
  );
10949
10996
  }
10997
+ if (Object.keys(projectRootFilesystem).length > 0) {
10998
+ if (typeof filesystem[CODEX_PROJECT_ROOTS_KEY] === "string") {
10999
+ logger?.warn(
11000
+ `"${CODEX_PROJECT_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions, but it will be overwritten by project-root rules. Consider removing the direct "${CODEX_PROJECT_ROOTS_KEY}" entry.`
11001
+ );
11002
+ }
11003
+ if (Object.keys(projectRootFilesystem).some((pattern) => pattern.includes("**"))) {
11004
+ filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
11005
+ }
11006
+ filesystem[CODEX_PROJECT_ROOTS_KEY] = projectRootFilesystem;
11007
+ }
10950
11008
  return {
10951
11009
  ...Object.keys(filesystem).length > 0 ? { filesystem } : {},
10952
11010
  ...Object.keys(domains).length > 0 ? { network: { domains } } : {}
@@ -10958,13 +11016,14 @@ function convertCodexProfileToRulesync(profile) {
10958
11016
  permission.read = {};
10959
11017
  permission.edit = {};
10960
11018
  for (const [pattern, access] of Object.entries(profile.filesystem)) {
10961
- if (access === "none") {
10962
- permission.read[pattern] = "deny";
10963
- permission.edit[pattern] = "deny";
10964
- } else if (access === "read") {
10965
- permission.read[pattern] = "allow";
10966
- } else {
10967
- permission.edit[pattern] = "allow";
11019
+ if (isCodexFilesystemAccess(access)) {
11020
+ addRulesyncFilesystemRule(permission, pattern, access);
11021
+ continue;
11022
+ }
11023
+ if (isCodexFilesystemRuleTable(access)) {
11024
+ for (const [nestedPattern, nestedAccess] of Object.entries(access)) {
11025
+ addRulesyncFilesystemRule(permission, nestedPattern, nestedAccess);
11026
+ }
10968
11027
  }
10969
11028
  }
10970
11029
  }
@@ -10987,6 +11046,40 @@ function toCodexProfile(value) {
10987
11046
  ...domains ? { network: { domains } } : {}
10988
11047
  };
10989
11048
  }
11049
+ function addFilesystemRule({
11050
+ filesystem,
11051
+ projectRootFilesystem,
11052
+ pattern,
11053
+ access,
11054
+ logger
11055
+ }) {
11056
+ if (pattern.trim() === "") {
11057
+ logger?.warn("Skipping empty pattern in filesystem permissions.");
11058
+ return;
11059
+ }
11060
+ if (canBeCodexFilesystemRoot(pattern)) {
11061
+ filesystem[pattern] = access;
11062
+ return;
11063
+ }
11064
+ projectRootFilesystem[pattern] = access;
11065
+ }
11066
+ function canBeCodexFilesystemRoot(pattern) {
11067
+ return (0, import_node_path71.isAbsolute)(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith("~/") || pattern === "~" || pattern.startsWith(":");
11068
+ }
11069
+ function addRulesyncFilesystemRule(permission, pattern, access) {
11070
+ if (access === "none") {
11071
+ permission.read ??= {};
11072
+ permission.edit ??= {};
11073
+ permission.read[pattern] = "deny";
11074
+ permission.edit[pattern] = "deny";
11075
+ } else if (access === "read") {
11076
+ permission.read ??= {};
11077
+ permission.read[pattern] = "allow";
11078
+ } else {
11079
+ permission.edit ??= {};
11080
+ permission.edit[pattern] = "allow";
11081
+ }
11082
+ }
10990
11083
  function toMutableTable(value) {
10991
11084
  if (!value || typeof value !== "object" || Array.isArray(value)) {
10992
11085
  return {};
@@ -10997,8 +11090,33 @@ function toFilesystemRecord(value) {
10997
11090
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
10998
11091
  const result = {};
10999
11092
  for (const [key, raw] of Object.entries(value)) {
11000
- if (typeof raw !== "string") continue;
11001
- if (raw === "read" || raw === "write" || raw === "none") {
11093
+ if (isCodexFilesystemAccess(raw)) {
11094
+ result[key] = raw;
11095
+ continue;
11096
+ }
11097
+ if (key === "glob_scan_max_depth" && typeof raw === "number") {
11098
+ result[key] = raw;
11099
+ continue;
11100
+ }
11101
+ const nested = toCodexFilesystemRuleTable(raw);
11102
+ if (nested) {
11103
+ result[key] = nested;
11104
+ }
11105
+ }
11106
+ return Object.keys(result).length > 0 ? result : void 0;
11107
+ }
11108
+ function isCodexFilesystemAccess(value) {
11109
+ return value === "read" || value === "write" || value === "none";
11110
+ }
11111
+ function isCodexFilesystemRuleTable(value) {
11112
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
11113
+ return Object.values(value).every(isCodexFilesystemAccess);
11114
+ }
11115
+ function toCodexFilesystemRuleTable(value) {
11116
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
11117
+ const result = {};
11118
+ for (const [key, raw] of Object.entries(value)) {
11119
+ if (isCodexFilesystemAccess(raw)) {
11002
11120
  result[key] = raw;
11003
11121
  }
11004
11122
  }
@@ -13212,7 +13330,8 @@ var RulesyncSkillFrontmatterSchemaInternal = import_mini39.z.looseObject({
13212
13330
  "allowed-tools": import_mini39.z.optional(import_mini39.z.array(import_mini39.z.string())),
13213
13331
  model: import_mini39.z.optional(import_mini39.z.string()),
13214
13332
  "disable-model-invocation": import_mini39.z.optional(import_mini39.z.boolean()),
13215
- "scheduled-task": import_mini39.z.optional(import_mini39.z.boolean())
13333
+ "scheduled-task": import_mini39.z.optional(import_mini39.z.boolean()),
13334
+ paths: import_mini39.z.optional(import_mini39.z.union([import_mini39.z.string(), import_mini39.z.array(import_mini39.z.string())]))
13216
13335
  })
13217
13336
  ),
13218
13337
  codexcli: import_mini39.z.optional(
@@ -13987,7 +14106,8 @@ var ClaudecodeSkillFrontmatterSchema = import_mini43.z.looseObject({
13987
14106
  description: import_mini43.z.string(),
13988
14107
  "allowed-tools": import_mini43.z.optional(import_mini43.z.array(import_mini43.z.string())),
13989
14108
  model: import_mini43.z.optional(import_mini43.z.string()),
13990
- "disable-model-invocation": import_mini43.z.optional(import_mini43.z.boolean())
14109
+ "disable-model-invocation": import_mini43.z.optional(import_mini43.z.boolean()),
14110
+ paths: import_mini43.z.optional(import_mini43.z.union([import_mini43.z.string(), import_mini43.z.array(import_mini43.z.string())]))
13991
14111
  });
13992
14112
  var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
13993
14113
  constructor({
@@ -14060,6 +14180,7 @@ var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14060
14180
  ...frontmatter["disable-model-invocation"] !== void 0 && {
14061
14181
  "disable-model-invocation": frontmatter["disable-model-invocation"]
14062
14182
  },
14183
+ ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
14063
14184
  ...this.relativeDirPath === CLAUDE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true }
14064
14185
  };
14065
14186
  const rulesyncFrontmatter = {
@@ -14097,6 +14218,9 @@ var ClaudecodeSkill = class _ClaudecodeSkill extends ToolSkill {
14097
14218
  },
14098
14219
  ...rulesyncFrontmatter.claudecode?.["disable-model-invocation"] !== void 0 && {
14099
14220
  "disable-model-invocation": rulesyncFrontmatter.claudecode["disable-model-invocation"]
14221
+ },
14222
+ ...rulesyncFrontmatter.claudecode?.paths !== void 0 && {
14223
+ paths: rulesyncFrontmatter.claudecode.paths
14100
14224
  }
14101
14225
  };
14102
14226
  const settablePaths = _ClaudecodeSkill.getSettablePaths({ global });
package/dist/index.d.cts CHANGED
@@ -183,6 +183,7 @@ declare const RulesyncSkillFrontmatterSchemaInternal: z.ZodMiniObject<{
183
183
  model: z.ZodMiniOptional<z.ZodMiniString<string>>;
184
184
  "disable-model-invocation": z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
185
185
  "scheduled-task": z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
186
+ paths: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniArray<z.ZodMiniString<string>>]>>;
186
187
  }, z.core.$loose>>;
187
188
  codexcli: z.ZodMiniOptional<z.ZodMiniObject<{
188
189
  "short-description": z.ZodMiniOptional<z.ZodMiniString<string>>;
@@ -214,6 +215,7 @@ type RulesyncSkillFrontmatterInput = {
214
215
  model?: string;
215
216
  "disable-model-invocation"?: boolean;
216
217
  "scheduled-task"?: boolean;
218
+ paths?: string | string[];
217
219
  };
218
220
  codexcli?: {
219
221
  "short-description"?: string;
package/dist/index.d.ts CHANGED
@@ -183,6 +183,7 @@ declare const RulesyncSkillFrontmatterSchemaInternal: z.ZodMiniObject<{
183
183
  model: z.ZodMiniOptional<z.ZodMiniString<string>>;
184
184
  "disable-model-invocation": z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
185
185
  "scheduled-task": z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
186
+ paths: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniArray<z.ZodMiniString<string>>]>>;
186
187
  }, z.core.$loose>>;
187
188
  codexcli: z.ZodMiniOptional<z.ZodMiniObject<{
188
189
  "short-description": z.ZodMiniOptional<z.ZodMiniString<string>>;
@@ -214,6 +215,7 @@ type RulesyncSkillFrontmatterInput = {
214
215
  model?: string;
215
216
  "disable-model-invocation"?: boolean;
216
217
  "scheduled-task"?: boolean;
218
+ paths?: string | string[];
217
219
  };
218
220
  codexcli?: {
219
221
  "short-description"?: string;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  convertFromTool,
8
8
  generate,
9
9
  importFromTool
10
- } from "./chunk-3DM6NLND.js";
10
+ } from "./chunk-RXCRO26Z.js";
11
11
 
12
12
  // src/index.ts
13
13
  async function generate2(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "8.15.1",
3
+ "version": "8.17.0",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",