rulesync 16.13.0 → 16.15.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.
@@ -419,7 +419,8 @@ const permissionsProcessorToolTargetTuple = [
419
419
  "takt",
420
420
  "vibe",
421
421
  "warp",
422
- "zed"
422
+ "zed",
423
+ "zoocode"
423
424
  ];
424
425
  const checksProcessorToolTargetTuple = [
425
426
  "amp",
@@ -4396,11 +4397,26 @@ const KiloPermissionsOverrideSchema = z.looseObject({
4396
4397
  * rather than which commands are permitted — so it is a loose passthrough on
4397
4398
  * the same terms, merged into the top level of `.claude/settings.json`.
4398
4399
  *
4400
+ * Any other key is a plain top-level `settings.json` key (`editorMode`, `env`,
4401
+ * `model`, ...), deep-merged into the generated file verbatim so settings
4402
+ * Claude Code adds faster than an allowlist can track stay authorable.
4403
+ * The exceptions are the keys another feature owns (`hooks`) and `$schema`.
4404
+ * Keys the target file cannot honor — `Managed`-only and `~/.claude.json`-only
4405
+ * keys in either scope, plus user-scope keys at project scope — are dropped
4406
+ * with a warning rather than written where they would never apply. So are the
4407
+ * keys whose value is a command Claude Code executes (`apiKeyHelper`,
4408
+ * `statusLine`, ...): this file is shareable via `rulesync fetch`, and a file
4409
+ * named for restricting things is not where a command belongs — author those
4410
+ * in `.rulesync/hooks.jsonc` instead.
4411
+ *
4399
4412
  * @example
4400
4413
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
4401
4414
  * @example
4402
4415
  * { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
4416
+ * @example
4417
+ * { "editorMode": "vim", "env": { "MY_VAR": "1" } }
4403
4418
  * @see https://code.claude.com/docs/en/sandboxing
4419
+ * @see https://code.claude.com/docs/en/settings-reference
4404
4420
  */
4405
4421
  const ClaudecodePermissionsOverrideSchema = z.looseObject({
4406
4422
  permission: z.optional(ToolScopedPermissionSchema),
@@ -5075,6 +5091,15 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
5075
5091
  sandbox_workspace_write: z.optional(z.looseObject({})),
5076
5092
  apps: z.optional(z.looseObject({})),
5077
5093
  approvals_reviewer: z.optional(z.union([CodexApprovalsReviewerSchema, z.looseObject({})])),
5094
+ /**
5095
+ * The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
5096
+ * Not a permission surface, but like `apps` it is a top-level table with no
5097
+ * canonical category, and Codex adds keys to it faster than an explicit model
5098
+ * could track — so it is a loose passthrough written verbatim.
5099
+ *
5100
+ * @see https://developers.openai.com/codex/config-reference
5101
+ */
5102
+ tui: z.optional(z.looseObject({})),
5078
5103
  git_write_rules: z.optional(z.boolean())
5079
5104
  });
5080
5105
  /**
@@ -5345,6 +5370,7 @@ const RulesyncRuleFrontmatterSchema = z.object({
5345
5370
  systemPrompt: z.optional(z.enum(["append"])),
5346
5371
  contextFile: z.optional(z.enum(["override"]))
5347
5372
  })),
5373
+ roo: z.optional(z.looseObject({ mode: z.optional(z.string()) })),
5348
5374
  takt: z.optional(z.looseObject({
5349
5375
  name: z.optional(z.string()),
5350
5376
  extends: z.optional(z.string()),
@@ -7461,7 +7487,8 @@ const CODEXCLI_OVERRIDE_KEYS = [
7461
7487
  "sandbox_mode",
7462
7488
  "sandbox_workspace_write",
7463
7489
  "apps",
7464
- "approvals_reviewer"
7490
+ "approvals_reviewer",
7491
+ "tui"
7465
7492
  ];
7466
7493
  //#endregion
7467
7494
  //#region src/features/shared/shared-config-gateway.ts
@@ -7786,7 +7813,9 @@ const SHARED_CONFIG_OWNERSHIP = {
7786
7813
  ownedKeys: [
7787
7814
  "chat.tools.terminal.autoApprove",
7788
7815
  "chat.tools.edits.autoApprove",
7789
- "chat.tools.urls.autoApprove"
7816
+ "chat.tools.urls.autoApprove",
7817
+ "zoo-code.allowedCommands",
7818
+ "zoo-code.deniedCommands"
7790
7819
  ]
7791
7820
  } }
7792
7821
  },
@@ -12750,6 +12779,21 @@ const ROO_DIR = ".roo";
12750
12779
  const ROO_COMMANDS_DIR_PATH = join(ROO_DIR, "commands");
12751
12780
  const ROO_SKILLS_DIR_PATH = join(ROO_DIR, "skills");
12752
12781
  const ROO_MCP_FILE_NAME = "mcp.json";
12782
+ /**
12783
+ * Mode slugs Roo/Zoo Code themselves accept for a `rules-{mode}` directory:
12784
+ * the loader builds the directory name by interpolating the active mode slug,
12785
+ * and custom-mode slugs are restricted to this alphabet. Validating against it
12786
+ * also keeps an authored value from escaping `.roo/` through path separators or
12787
+ * `..` segments.
12788
+ */
12789
+ const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
12790
+ /**
12791
+ * `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
12792
+ * INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
12793
+ * same in global scope, where it resolves under `~/.roo/`.
12794
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
12795
+ */
12796
+ const rooModeRulesDirName = (mode) => `rules-${mode}`;
12753
12797
  const ROO_IGNORE_FILE_NAME = ".rooignore";
12754
12798
  /**
12755
12799
  * Roo Code reads project-level custom modes from a single aggregated
@@ -27775,6 +27819,27 @@ const defaultGetFactory$3 = (target) => {
27775
27819
  if (!factory) throw new Error(`Unsupported tool target: ${target}`);
27776
27820
  return factory;
27777
27821
  };
27822
+ /**
27823
+ * Warn about per-server tool filters the target tool cannot express.
27824
+ *
27825
+ * `enabledTools`/`disabledTools` are canonical fields, so a single
27826
+ * `.rulesync/.mcp.json` can carry a filter that only some targets read. The
27827
+ * unsupported ones are stripped before generation (writing them would produce a
27828
+ * key the tool discards on load), which used to happen silently — the filter
27829
+ * simply did not apply and nothing said so. Warning names the servers whose
27830
+ * filter is being dropped for this target so the gap is visible at generate
27831
+ * time rather than in the tool's behavior.
27832
+ *
27833
+ * Only fields actually present are reported, so a config that never authored
27834
+ * the filter stays quiet.
27835
+ */
27836
+ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger }) {
27837
+ for (const field of fields) {
27838
+ const serverNames = Object.entries(mcpServers ?? {}).filter(([, serverConfig]) => serverConfig[field] !== void 0).map(([serverName]) => serverName);
27839
+ if (serverNames.length === 0) continue;
27840
+ logger.warn(`${toolTarget} does not read the per-server \`${field}\` MCP tool filter; dropping it from ${serverNames.join(", ")}.`);
27841
+ }
27842
+ }
27778
27843
  var McpProcessor = class extends FeatureProcessor {
27779
27844
  toolTarget;
27780
27845
  global;
@@ -27854,6 +27919,12 @@ var McpProcessor = class extends FeatureProcessor {
27854
27919
  const fieldsToStrip = [];
27855
27920
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
27856
27921
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
27922
+ warnStrippedMcpServerFields({
27923
+ mcpServers: targetedRulesyncMcp.getJson().mcpServers,
27924
+ fields: fieldsToStrip,
27925
+ toolTarget: this.toolTarget,
27926
+ logger: this.logger
27927
+ });
27857
27928
  const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
27858
27929
  return await factory.class.fromRulesyncMcp({
27859
27930
  outputRoot: this.outputRoot,
@@ -29334,6 +29405,27 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
29334
29405
  return { permission };
29335
29406
  }
29336
29407
  //#endregion
29408
+ //#region src/utils/control-characters.ts
29409
+ /**
29410
+ * Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
29411
+ * introducer U+009B), the bidirectional overrides and isolates, and the Unicode
29412
+ * line and paragraph separators, and the plain LRM/RLM marks. A name or value
29413
+ * copied out of an untrusted config file, a fetched repository, or a tool's own
29414
+ * settings file must never reach the terminal with these intact: they let the
29415
+ * text forge log lines, reorder what is printed around them, or inject escape
29416
+ * sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
29417
+ * neutral characters beside them, so they go too — a diagnostic line is not the
29418
+ * place to preserve the typography of a right-to-left name.
29419
+ */
29420
+ const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
29421
+ /**
29422
+ * Removes every control character from `text` so it is safe to splice into a
29423
+ * log line or other terminal output.
29424
+ */
29425
+ function stripControlCharacters(text) {
29426
+ return text.replace(CONTROL_CHARACTERS_PATTERN, "");
29427
+ }
29428
+ //#endregion
29337
29429
  //#region src/features/permissions/claudecode-permissions.ts
29338
29430
  /**
29339
29431
  * Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
@@ -29418,8 +29510,9 @@ function deepMergeRecords(base, patch) {
29418
29510
  * emits them only under `--global`.
29419
29511
  *
29420
29512
  * Deliberately NOT listed:
29421
- * - `bwrapPath` / `socatPath`: v2.1.232 added them to the managed-settings
29422
- * approval dialog, which is a consent prompt, not a project-scope rejection.
29513
+ * - `ripgrep` / `bwrapPath` / `socatPath`: each names an executable, so
29514
+ * `stripCommandExecutingSandboxPaths` refuses them in both scopes rather than
29515
+ * emitting them under `--global`.
29423
29516
  * - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
29424
29517
  * unit is the individual entry's mode, not the settings key, and the same
29425
29518
  * lists carry `deny` entries that project settings *do* honor — dropping a
@@ -29427,7 +29520,6 @@ function deepMergeRecords(base, patch) {
29427
29520
  * filters those lists per entry instead.
29428
29521
  *
29429
29522
  * @see https://code.claude.com/docs/en/sandboxing
29430
- * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
29431
29523
  */
29432
29524
  const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29433
29525
  ["filesystem", "disabled"],
@@ -29436,10 +29528,228 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29436
29528
  ["credentials", "allowPlaintextInject"],
29437
29529
  ["credentials", "awsPairs"],
29438
29530
  ["credentials", "sigv4"],
29439
- ["allowAppleEvents"],
29440
- ["ripgrep"]
29531
+ ["allowAppleEvents"]
29532
+ ];
29533
+ /**
29534
+ * Walks `segments` from `root`, returning the record they name or `undefined` if
29535
+ * any step is missing or not a record. Shared by everything below that addresses
29536
+ * a `sandbox` path, so a nested path added to one of the tables is actually
29537
+ * traversed rather than silently skipped.
29538
+ */
29539
+ function resolveSandboxParent({ root, segments }) {
29540
+ let parent = root;
29541
+ for (const segment of segments) {
29542
+ const next = parent[segment];
29543
+ if (!isPlainRecord(next)) return void 0;
29544
+ parent = next;
29545
+ }
29546
+ return parent;
29547
+ }
29548
+ /**
29549
+ * Deletes `path` from `target` in place and reports whether anything was there,
29550
+ * dropping a container the removal emptied so no `"network": {}` noise is left
29551
+ * behind.
29552
+ */
29553
+ function deleteSandboxPath({ target, path }) {
29554
+ const leaf = path.at(-1);
29555
+ if (leaf === void 0) return false;
29556
+ const parentPath = path.slice(0, -1);
29557
+ const parent = resolveSandboxParent({
29558
+ root: target,
29559
+ segments: parentPath
29560
+ });
29561
+ if (parent === void 0 || parent[leaf] === void 0) return false;
29562
+ delete parent[leaf];
29563
+ for (let depth = parentPath.length; depth > 0; depth--) {
29564
+ const container = resolveSandboxParent({
29565
+ root: target,
29566
+ segments: parentPath.slice(0, depth)
29567
+ });
29568
+ if (container === void 0 || Object.keys(container).length > 0) break;
29569
+ const holder = resolveSandboxParent({
29570
+ root: target,
29571
+ segments: parentPath.slice(0, depth - 1)
29572
+ });
29573
+ const name = parentPath[depth - 1];
29574
+ if (holder === void 0 || name === void 0) break;
29575
+ delete holder[name];
29576
+ }
29577
+ return true;
29578
+ }
29579
+ /**
29580
+ * The `permissions.defaultMode` values that start a session with fewer prompts
29581
+ * than the default. `plan` and `default` are absent because they do not widen
29582
+ * anything.
29583
+ */
29584
+ const CLAUDECODE_WIDENING_DEFAULT_MODES = {
29585
+ acceptEdits: "every file edit is then applied without a prompt",
29586
+ auto: "shell commands are then auto-approved by a classifier rather than by you",
29587
+ bypassPermissions: "every session then starts with no permission prompts at all"
29588
+ };
29589
+ /**
29590
+ * The `permissions` fields that widen rather than restrict: a `defaultMode` that
29591
+ * removes prompts, and `additionalDirectories`, which moves the
29592
+ * working-directory boundary. Warned for the same reason `disableAllHooks` is: a
29593
+ * shareable permissions file should not loosen the permission system quietly.
29594
+ */
29595
+ function warnOnWideningPermissionFields({ fields, relativeFilePath, logger }) {
29596
+ const defaultMode = fields.defaultMode;
29597
+ if (typeof defaultMode === "string" && Object.hasOwn(CLAUDECODE_WIDENING_DEFAULT_MODES, defaultMode)) logger?.warn(`Claude Code permissions: writing 'permissions.defaultMode: "${defaultMode}"' to ${relativeFilePath}; ${CLAUDECODE_WIDENING_DEFAULT_MODES[defaultMode]}. Review it as you would a hook, especially if this permissions file came from 'rulesync fetch'.`);
29598
+ const additionalDirectories = fields.additionalDirectories;
29599
+ if (additionalDirectories !== void 0 && !(Array.isArray(additionalDirectories) && additionalDirectories.length === 0)) logger?.warn(`Claude Code permissions: writing 'permissions.additionalDirectories' to ${relativeFilePath}; it moves the boundary of what Claude Code may read and edit outside the project. Review the paths, especially if this permissions file came from 'rulesync fetch'.`);
29600
+ }
29601
+ /**
29602
+ * `sandbox` paths whose value names a binary Claude Code runs. `sandbox` has its
29603
+ * own merge branch, so the top-level refusal in `stripUnhonoredTopLevelKeys`
29604
+ * never sees them — they are refused here on the same grounds, in both scopes:
29605
+ * a fetched `.rulesync/permissions.jsonc` must not be able to point Claude Code
29606
+ * at an executable of its choosing.
29607
+ *
29608
+ * @see https://code.claude.com/docs/en/sandboxing
29609
+ */
29610
+ const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
29611
+ ["ripgrep"],
29612
+ ["bwrapPath"],
29613
+ ["socatPath"]
29614
+ ];
29615
+ /**
29616
+ * `sandbox` paths that loosen the sandbox rather than naming something to run:
29617
+ * they let commands out of it, weaken the isolation it provides, or redirect
29618
+ * where its traffic goes. They are written like `env` is — the ordinary uses are
29619
+ * too common to refuse — but never silently, because a fetched override should
29620
+ * not be able to open the sandbox without saying so. `widens` keeps the warning
29621
+ * to the value that actually loosens the policy, so authoring the restrictive
29622
+ * value (`allowUnsandboxedCommands: false`, an empty `excludedCommands`) stays
29623
+ * quiet. The `allow*` lists are here for a structural reason: Claude Code merges
29624
+ * a list across every settings scope rather than replacing it, so a project file
29625
+ * can only ever add to them. Their counterparts — `denyRead`, `denyWrite`,
29626
+ * `deniedDomains` — merge the same way, but adding to a deny list only ever
29627
+ * narrows the policy, so they are absent.
29628
+ *
29629
+ * @see https://code.claude.com/docs/en/sandboxing
29630
+ */
29631
+ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
29632
+ {
29633
+ path: ["allowAppleEvents"],
29634
+ reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
29635
+ widens: (value) => value === true
29636
+ },
29637
+ {
29638
+ path: ["allowUnsandboxedCommands"],
29639
+ reason: "controls whether Claude may retry a blocked command outside the sandbox",
29640
+ widens: (value) => value !== false
29641
+ },
29642
+ {
29643
+ path: ["autoAllowBashIfSandboxed"],
29644
+ reason: "controls whether every Bash command the sandbox accepts runs without a prompt",
29645
+ widens: (value) => value !== false
29646
+ },
29647
+ {
29648
+ path: ["enableWeakerNestedSandbox"],
29649
+ reason: "runs the Linux sandbox inside an unprivileged container, which weakens it",
29650
+ widens: (value) => value === true
29651
+ },
29652
+ {
29653
+ path: ["enableWeakerNetworkIsolation"],
29654
+ reason: "weakens the sandbox's network isolation on macOS",
29655
+ widens: (value) => value === true
29656
+ },
29657
+ {
29658
+ path: ["enabled"],
29659
+ reason: "turns the sandbox on, and sandboxed Bash commands then run without a permission prompt unless `autoAllowBashIfSandboxed` is false",
29660
+ widens: (value) => value === true
29661
+ },
29662
+ {
29663
+ path: ["excludedCommands"],
29664
+ reason: "names commands that always run outside the sandbox, with no sandbox policy applied",
29665
+ widens: (value) => !Array.isArray(value) || value.length > 0
29666
+ },
29667
+ {
29668
+ path: ["filesystem", "allowRead"],
29669
+ reason: "re-opens reading inside a region the sandbox's `denyRead` blocks",
29670
+ widens: (value) => !Array.isArray(value) || value.length > 0
29671
+ },
29672
+ {
29673
+ path: ["filesystem", "allowWrite"],
29674
+ reason: "adds paths sandboxed commands may write to, outside the working directory",
29675
+ widens: (value) => !Array.isArray(value) || value.length > 0
29676
+ },
29677
+ {
29678
+ path: ["ignoreViolations"],
29679
+ reason: "hides the sandbox violations it names, so a blocked access stops being reported",
29680
+ widens: (value) => isPlainRecord(value) ? Object.keys(value).length > 0 : value !== false
29681
+ },
29682
+ {
29683
+ path: ["network", "allowAllUnixSockets"],
29684
+ reason: "lets sandboxed commands connect to every Unix socket",
29685
+ widens: (value) => value === true
29686
+ },
29687
+ {
29688
+ path: ["network", "allowedDomains"],
29689
+ reason: "pre-allows domains sandboxed commands may reach without a prompt",
29690
+ widens: (value) => !Array.isArray(value) || value.length > 0
29691
+ },
29692
+ {
29693
+ path: ["network", "allowLocalBinding"],
29694
+ reason: "lets sandboxed commands bind local ports",
29695
+ widens: (value) => value === true
29696
+ },
29697
+ {
29698
+ path: ["network", "allowMachLookup"],
29699
+ reason: "names the macOS services sandboxed commands may reach, and `*` means every service",
29700
+ widens: (value) => !Array.isArray(value) || value.length > 0
29701
+ },
29702
+ {
29703
+ path: ["network", "allowUnixSockets"],
29704
+ reason: "names Unix sockets sandboxed commands may reach, and one such as `/var/run/docker.sock` is host access",
29705
+ widens: (value) => !Array.isArray(value) || value.length > 0
29706
+ },
29707
+ {
29708
+ path: ["network", "httpProxyPort"],
29709
+ reason: "routes the sandbox's HTTP traffic through the port it names",
29710
+ widens: () => true
29711
+ },
29712
+ {
29713
+ path: ["network", "socksProxyPort"],
29714
+ reason: "routes the sandbox's SOCKS traffic through the port it names",
29715
+ widens: () => true
29716
+ }
29441
29717
  ];
29442
29718
  /**
29719
+ * Warns once per authored `sandbox` path that loosens the sandbox. Nothing is
29720
+ * removed — the value is written, just not silently. Called on the filtered
29721
+ * `sandbox` so it never claims to be writing a path the scope filters dropped.
29722
+ */
29723
+ function warnOnTrustAffectingSandboxPaths({ sandbox, relativeFilePath, logger }) {
29724
+ for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
29725
+ const leaf = path.at(-1);
29726
+ if (leaf === void 0) continue;
29727
+ const parent = resolveSandboxParent({
29728
+ root: sandbox,
29729
+ segments: path.slice(0, -1)
29730
+ });
29731
+ if (parent === void 0) continue;
29732
+ const value = parent[leaf];
29733
+ if (value === void 0 || !widens(value)) continue;
29734
+ logger?.warn(`Claude Code permissions: writing 'sandbox.${path.join(".")}' to ${relativeFilePath}; it ${reason}. Review the value as you would a hook, especially if this permissions file came from 'rulesync fetch'.`);
29735
+ }
29736
+ }
29737
+ /**
29738
+ * Copy of the authored `sandbox` override with the paths that name an
29739
+ * executable removed, warning once per dropped path.
29740
+ */
29741
+ function stripCommandExecutingSandboxPaths({ sandbox, relativeFilePath, logger }) {
29742
+ const filtered = structuredClone(sandbox);
29743
+ for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) {
29744
+ if (!deleteSandboxPath({
29745
+ target: filtered,
29746
+ path
29747
+ })) continue;
29748
+ logger?.warn(`Claude Code permissions: 'sandbox.${path.join(".")}' names an executable Claude Code runs, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; set this path in ${relativeFilePath} by hand.`);
29749
+ }
29750
+ return filtered;
29751
+ }
29752
+ /**
29443
29753
  * Copy of the authored `sandbox` override with the user/managed-only paths
29444
29754
  * removed, warning once per dropped path. Only the override copy is filtered —
29445
29755
  * a value already hand-written in the target file is left untouched, matching
@@ -29448,24 +29758,10 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29448
29758
  function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
29449
29759
  const filtered = structuredClone(sandbox);
29450
29760
  for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
29451
- const leaf = path.at(-1);
29452
- if (leaf === void 0) continue;
29453
- const parentPath = path.slice(0, -1);
29454
- let parent = filtered;
29455
- for (const segment of parentPath) {
29456
- const next = parent[segment];
29457
- if (!isPlainRecord(next)) {
29458
- parent = {};
29459
- break;
29460
- }
29461
- parent = next;
29462
- }
29463
- if (parent[leaf] === void 0) continue;
29464
- delete parent[leaf];
29465
- const [container] = parentPath;
29466
- if (container !== void 0 && isPlainRecord(filtered[container])) {
29467
- if (Object.keys(filtered[container]).length === 0) delete filtered[container];
29468
- }
29761
+ if (!deleteSandboxPath({
29762
+ target: filtered,
29763
+ path
29764
+ })) continue;
29469
29765
  logger?.warn(`Claude Code permissions: 'sandbox.${path.join(".")}' is only honored in user/managed/--settings settings, so it is not written to the project-scoped ${relativeFilePath}. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
29470
29766
  }
29471
29767
  return filtered;
@@ -29521,6 +29817,215 @@ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
29521
29817
  else filtered.credentials = filteredCredentials;
29522
29818
  return filtered;
29523
29819
  }
29820
+ /**
29821
+ * Top-level `.claude/settings.json` keys another feature owns, derived from
29822
+ * {@link SHARED_CONFIG_OWNERSHIP} rather than restated here so a feature that
29823
+ * starts owning a new key is excluded from the passthrough automatically
29824
+ * (today: `hooks`, from the hooks feature). Only `replace-owned-keys` entries
29825
+ * name keys; the `custom` policies on this file (`ignore`, `permissions`) own
29826
+ * entries *inside* `permissions`, which the passthrough excludes wholesale.
29827
+ */
29828
+ const CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS = Object.entries(SHARED_CONFIG_OWNERSHIP[".claude/settings.json"]?.features ?? {}).flatMap(([feature, policy]) => feature !== "permissions" && policy.kind === "replace-owned-keys" ? policy.ownedKeys : []);
29829
+ /**
29830
+ * Top-level `.claude/settings.json` keys the generic `claudecode` override
29831
+ * passthrough must not carry. `permissions` and `sandbox` have their own merge
29832
+ * branches (the managed `allow`/`ask`/`deny` arrays and the scope filtering
29833
+ * respectively), `permission` is rulesync's own canonical tool-scoped block
29834
+ * rather than a settings key, `$schema` is an editor pointer rather than a
29835
+ * Claude Code setting, and the rest belong to the other features writing this
29836
+ * shared file.
29837
+ */
29838
+ const CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
29839
+ "permission",
29840
+ "permissions",
29841
+ "sandbox",
29842
+ "$schema",
29843
+ ...CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS
29844
+ ]);
29845
+ /**
29846
+ * Top-level settings keys Claude Code reads only from user settings, managed
29847
+ * settings and the `--settings` CLI flag — the same restriction
29848
+ * `CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS` records for the `sandbox` subtree, and
29849
+ * the reason the passthrough drops them at project scope instead of committing
29850
+ * a setting that never applies. `rulesync generate --global` writes the user
29851
+ * settings file, so they are emitted there.
29852
+ *
29853
+ * Derived from the per-key **Scope** column of the settings reference: every
29854
+ * top-level key documented as `User or managed` or `User, local, or managed`.
29855
+ *
29856
+ * @see https://code.claude.com/docs/en/settings-reference
29857
+ */
29858
+ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
29859
+ "askUserQuestionTimeout",
29860
+ "autoMode",
29861
+ "dialogExpiry",
29862
+ "enableArtifact",
29863
+ "footerLinksRegexes",
29864
+ "pluginConfigs",
29865
+ "skipAutoPermissionPrompt",
29866
+ "skipDangerousModePermissionPrompt",
29867
+ "spellcheck",
29868
+ "sshConfigs",
29869
+ "syncClaudeAiSkills",
29870
+ "useAutoModeDuringPlan",
29871
+ "vimInsertModeRemaps"
29872
+ ]);
29873
+ /**
29874
+ * Top-level settings keys neither file rulesync writes can honor, with the file
29875
+ * that does. `Managed` keys are read only from the settings file an organization
29876
+ * deploys, and `Global config` keys only from `~/.claude.json` — rulesync writes
29877
+ * `.claude/settings.json` and `~/.claude/settings.json`, so authoring either
29878
+ * kind through the override would produce a policy that silently never applies.
29879
+ *
29880
+ * Derived from the per-key **Scope** column of the settings reference.
29881
+ *
29882
+ * @see https://code.claude.com/docs/en/settings-reference
29883
+ */
29884
+ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
29885
+ allowAllClaudeAiMcps: "managed settings",
29886
+ allowedChannelPlugins: "managed settings",
29887
+ allowManagedHooksOnly: "managed settings",
29888
+ allowManagedMcpServersOnly: "managed settings",
29889
+ allowManagedPermissionRulesOnly: "managed settings",
29890
+ autoConnectIde: "~/.claude.json",
29891
+ autoInstallIdeExtension: "~/.claude.json",
29892
+ blockedMarketplaces: "managed settings",
29893
+ browserExternalPageTools: "managed settings",
29894
+ channelsEnabled: "managed settings",
29895
+ claudeMd: "managed settings",
29896
+ diffTool: "~/.claude.json",
29897
+ disableBrowserExternalNavigation: "managed settings",
29898
+ disableCommandPluginSources: "managed settings",
29899
+ disableMobileSimulatorTools: "managed settings",
29900
+ disableSideloadFlags: "managed settings",
29901
+ externalEditorContext: "~/.claude.json",
29902
+ forceLoginGatewayUrl: "managed settings",
29903
+ forceRemoteSettingsRefresh: "managed settings",
29904
+ parentSettingsBehavior: "managed settings",
29905
+ permissionExplainerEnabled: "~/.claude.json",
29906
+ pluginSuggestionMarketplaces: "managed settings",
29907
+ pluginTrustMessage: "managed settings",
29908
+ requiredMaximumVersion: "managed settings",
29909
+ requiredMinimumVersion: "managed settings",
29910
+ sshHostAllowlist: "managed settings",
29911
+ strictKnownMarketplaces: "managed settings",
29912
+ strictPluginOnlyCustomization: "managed settings",
29913
+ teammateDefaultModel: "~/.claude.json",
29914
+ wslInheritsWindowsSettings: "managed settings"
29915
+ };
29916
+ /**
29917
+ * Top-level settings keys whose value Claude Code **executes**. The generic
29918
+ * passthrough refuses them outright rather than warning: `.rulesync/*` files
29919
+ * are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
29920
+ * straight into a project — and a file named for *restricting* what an agent
29921
+ * may do is not somewhere a reviewer looks for a command to run. Commands
29922
+ * belong in `.rulesync/hooks.jsonc` and `.rulesync/.mcp.json`, which are read
29923
+ * as executable by anyone reviewing them. Set these by hand in the settings
29924
+ * file if you need them.
29925
+ *
29926
+ * The value is the reason, spliced into the warning.
29927
+ *
29928
+ * @see https://code.claude.com/docs/en/settings-reference
29929
+ */
29930
+ const CLAUDECODE_COMMAND_EXECUTING_KEYS = {
29931
+ apiKeyHelper: "runs the script it names to mint an API key",
29932
+ awsAuthRefresh: "runs the command it names to refresh AWS credentials",
29933
+ awsCredentialExport: "runs the command it names to export AWS credentials",
29934
+ fileSuggestion: "runs its `command` on every `@` file completion",
29935
+ gcpAuthRefresh: "runs the command it names to refresh Google Cloud credentials",
29936
+ otelHeadersHelper: "runs the script it names to build OpenTelemetry headers",
29937
+ policyHelper: "runs the executable it names to compute the managed settings",
29938
+ processWrapper: "wraps every process Claude Code spawns",
29939
+ statusLine: "runs its `command` on every status-line render",
29940
+ subagentStatusLine: "runs its `command` on every subagent status row"
29941
+ };
29942
+ /**
29943
+ * Top-level settings keys the passthrough does write, but never silently: each
29944
+ * one widens what Claude Code trusts or where it sends data, so a value that
29945
+ * arrived with a fetched `.rulesync/permissions.jsonc` should be looked at
29946
+ * deliberately. Warning on write follows the precedent set for Warp's
29947
+ * `command_denylist`, which also replaces a protection when rulesync writes it.
29948
+ *
29949
+ * The value is the reason, spliced into the warning.
29950
+ */
29951
+ const CLAUDECODE_TRUST_AFFECTING_KEYS = {
29952
+ agent: "starts every session as the named subagent, with that subagent's prompt, tools and model",
29953
+ allowedHttpHookUrls: "limits which URLs an HTTP hook may target, and an empty list means every URL",
29954
+ allowedMcpServers: "allowlists the MCP servers that may be used, and entries from every settings file merge into one list, so an entry here widens an allowlist deployed elsewhere",
29955
+ autoMode: "auto-approves shell commands with a classifier rather than with a prompt",
29956
+ disableAllHooks: "controls whether hooks run at all",
29957
+ disableSkillShellExecution: "re-opens the inline shell commands in a skill or custom command that a user setting had turned off",
29958
+ enableAllProjectMcpServers: "auto-approves every server in the project `.mcp.json`",
29959
+ enabledMcpjsonServers: "auto-approves the named servers in the project `.mcp.json`",
29960
+ enabledPlugins: "enables plugins, which can ship their own hooks",
29961
+ env: "sets environment variables for every process Claude Code spawns, so a value such as `NODE_OPTIONS` or `PATH` runs code and `ANTHROPIC_BASE_URL` redirects every prompt",
29962
+ extraKnownMarketplaces: "registers plugin marketplace sources",
29963
+ httpHookAllowedEnvVars: "controls which environment variables an HTTP hook may put in a request header, credentials included",
29964
+ outputStyle: "replaces the system prompt every session runs with",
29965
+ skipAutoPermissionPrompt: "removes the confirmation shown before auto-approval mode starts",
29966
+ skipDangerousModePermissionPrompt: "removes the confirmation shown before the mode that skips every permission check starts"
29967
+ };
29968
+ /**
29969
+ * The keys from the table above that only widen at one particular value.
29970
+ * `disableSkillShellExecution: true` turns inline shell execution off, which
29971
+ * restricts; the `false` that turns it back on is what a fetched override could
29972
+ * use to undo a user setting, so only that value is warned about.
29973
+ */
29974
+ const CLAUDECODE_TRUST_KEY_WIDENING_VALUES = { disableSkillShellExecution: (value) => value === false };
29975
+ /**
29976
+ * A key name is authored data that ends up in a log line, so strip the control
29977
+ * characters that would let it forge a line or hide the warnings beside it, and
29978
+ * cap the length.
29979
+ */
29980
+ function displayKey(key) {
29981
+ const stripped = stripControlCharacters(key);
29982
+ return stripped.length > 80 ? `${stripped.slice(0, 80)}…` : stripped;
29983
+ }
29984
+ /**
29985
+ * Alternate spellings Claude Code accepts for a top-level settings key, mapped
29986
+ * to the canonical key whose **Scope** the alias inherits: "In any settings
29987
+ * file that accepts the canonical key, Claude Code reads the alias exactly as
29988
+ * it reads the canonical key." Resolving through this map before the scope
29989
+ * check keeps an alias from slipping past a restriction its canonical spelling
29990
+ * is caught by — `allowedMarketplaces` is `Managed`, like
29991
+ * `strictKnownMarketplaces`. Both aliases require Claude Code v2.1.232+.
29992
+ *
29993
+ * @see https://code.claude.com/docs/en/settings-reference#marketplace-key-aliases
29994
+ */
29995
+ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
29996
+ additionalMarketplaces: "extraKnownMarketplaces",
29997
+ allowedMarketplaces: "strictKnownMarketplaces"
29998
+ };
29999
+ /**
30000
+ * Copy of the authored top-level passthrough with the keys the target file
30001
+ * cannot honor removed, warning once per dropped key. Like
30002
+ * `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a value
30003
+ * already hand-written in the target file is left untouched, which is why the
30004
+ * warning points at it.
30005
+ */
30006
+ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logger }) {
30007
+ const filtered = {};
30008
+ for (const [key, value] of Object.entries(overrides)) {
30009
+ const shown = displayKey(key);
30010
+ const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
30011
+ if (Object.hasOwn(CLAUDECODE_COMMAND_EXECUTING_KEYS, canonicalKey)) {
30012
+ logger?.warn(`Claude Code permissions: '${shown}' ${CLAUDECODE_COMMAND_EXECUTING_KEYS[canonicalKey]}, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; author commands in .rulesync/hooks.jsonc, or set this key in ${relativeFilePath} by hand.`);
30013
+ continue;
30014
+ }
30015
+ if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) {
30016
+ logger?.warn(`Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
30017
+ continue;
30018
+ }
30019
+ if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
30020
+ logger?.warn(`Claude Code permissions: '${shown}' is not honored in the project-scoped ${relativeFilePath}, so it is not written there — Claude Code reads it from user, local or managed settings. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
30021
+ continue;
30022
+ }
30023
+ const widensAtValue = CLAUDECODE_TRUST_KEY_WIDENING_VALUES[canonicalKey];
30024
+ if (Object.hasOwn(CLAUDECODE_TRUST_AFFECTING_KEYS, canonicalKey) && (widensAtValue === void 0 || widensAtValue(value))) logger?.warn(`Claude Code permissions: writing '${shown}' to ${relativeFilePath}; it ${CLAUDECODE_TRUST_AFFECTING_KEYS[canonicalKey]}. Review the value as you would a hook, especially if this permissions file came from 'rulesync fetch'.`);
30025
+ filtered[key] = value;
30026
+ }
30027
+ return filtered;
30028
+ }
29524
30029
  const CLAUDE_PATH_RULE_ALIASES = {
29525
30030
  Write: "Edit",
29526
30031
  NotebookEdit: "Edit",
@@ -29591,7 +30096,13 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29591
30096
  });
29592
30097
  const overridePermissions = config.claudecode?.permissions;
29593
30098
  if (overridePermissions && typeof overridePermissions === "object") {
29594
- const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
30099
+ const { allow: _a, ask: _k, deny: _d, ...rest } = overridePermissions;
30100
+ const nonListFields = Object.fromEntries(Object.entries(rest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
30101
+ warnOnWideningPermissionFields({
30102
+ fields: nonListFields,
30103
+ relativeFilePath: paths.relativeFilePath,
30104
+ logger
30105
+ });
29595
30106
  settings.permissions = {
29596
30107
  ...settings.permissions,
29597
30108
  ...nonListFields
@@ -29599,17 +30110,41 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29599
30110
  }
29600
30111
  const overrideSandbox = config.claudecode?.sandbox;
29601
30112
  if (isPlainRecord(overrideSandbox)) {
29602
- const scopedSandbox = global ? overrideSandbox : stripProjectIgnoredMaskEntries({
30113
+ const executableFreeSandbox = stripCommandExecutingSandboxPaths({
30114
+ sandbox: overrideSandbox,
30115
+ relativeFilePath: paths.relativeFilePath,
30116
+ logger
30117
+ });
30118
+ const scopedSandbox = global ? executableFreeSandbox : stripProjectIgnoredMaskEntries({
29603
30119
  sandbox: stripGlobalOnlySandboxPaths({
29604
- sandbox: overrideSandbox,
30120
+ sandbox: executableFreeSandbox,
29605
30121
  relativeFilePath: paths.relativeFilePath,
29606
30122
  logger
29607
30123
  }),
29608
30124
  relativeFilePath: paths.relativeFilePath,
29609
30125
  logger
29610
30126
  });
30127
+ warnOnTrustAffectingSandboxPaths({
30128
+ sandbox: scopedSandbox,
30129
+ relativeFilePath: paths.relativeFilePath,
30130
+ logger
30131
+ });
29611
30132
  if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
29612
30133
  }
30134
+ const overrideTopLevel = {};
30135
+ for (const [key, value] of Object.entries(config.claudecode ?? {})) {
30136
+ if (CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS.has(key)) continue;
30137
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
30138
+ if (value === void 0) continue;
30139
+ overrideTopLevel[key] = value;
30140
+ }
30141
+ const scopedTopLevel = stripUnhonoredTopLevelKeys({
30142
+ overrides: overrideTopLevel,
30143
+ global,
30144
+ relativeFilePath: paths.relativeFilePath,
30145
+ logger
30146
+ });
30147
+ if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
29613
30148
  const managedToolNames = managedClaudeToolNames(config);
29614
30149
  const merged = applyPermissions({
29615
30150
  settings,
@@ -29642,12 +30177,33 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29642
30177
  ask: permissions.ask ?? [],
29643
30178
  deny: permissions.deny ?? []
29644
30179
  });
29645
- const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
30180
+ const { allow: _a, ask: _k, deny: _d, ...permissionsRest } = permissions;
30181
+ const nonListFields = Object.fromEntries(Object.entries(permissionsRest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
29646
30182
  if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
29647
30183
  const { sandbox } = settings;
29648
- if (isPlainRecord(sandbox) && Object.keys(sandbox).length > 0) config.claudecode = {
30184
+ if (isPlainRecord(sandbox)) {
30185
+ const importedSandbox = structuredClone(sandbox);
30186
+ for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) deleteSandboxPath({
30187
+ target: importedSandbox,
30188
+ path
30189
+ });
30190
+ if (Object.keys(importedSandbox).length > 0) config.claudecode = {
30191
+ ...config.claudecode,
30192
+ sandbox: importedSandbox
30193
+ };
30194
+ }
30195
+ const topLevelPassthrough = {};
30196
+ for (const [key, value] of Object.entries(settings)) {
30197
+ if (CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS.has(key)) continue;
30198
+ const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
30199
+ if (Object.hasOwn(CLAUDECODE_COMMAND_EXECUTING_KEYS, canonicalKey)) continue;
30200
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
30201
+ if (value === void 0) continue;
30202
+ topLevelPassthrough[key] = value;
30203
+ }
30204
+ if (Object.keys(topLevelPassthrough).length > 0) config.claudecode = {
29649
30205
  ...config.claudecode,
29650
- sandbox
30206
+ ...topLevelPassthrough
29651
30207
  };
29652
30208
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
29653
30209
  }
@@ -33507,7 +34063,7 @@ function extractKiroOverride(toolsSettings) {
33507
34063
  if (Object.keys(overrideToolsSettings).length === 0) return void 0;
33508
34064
  return { toolsSettings: overrideToolsSettings };
33509
34065
  }
33510
- function asStringArray(value) {
34066
+ function asStringArray$1(value) {
33511
34067
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
33512
34068
  }
33513
34069
  /**
@@ -33517,8 +34073,8 @@ function asStringArray(value) {
33517
34073
  */
33518
34074
  function rulesFromArrays(settings, allowKey, denyKey) {
33519
34075
  const rules = {};
33520
- for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
33521
- for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
34076
+ for (const pattern of asStringArray$1(settings[allowKey])) rules[pattern] = "allow";
34077
+ for (const pattern of asStringArray$1(settings[denyKey])) rules[pattern] = "deny";
33522
34078
  return rules;
33523
34079
  }
33524
34080
  //#endregion
@@ -36430,6 +36986,181 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
36430
36986
  }
36431
36987
  };
36432
36988
  //#endregion
36989
+ //#region src/constants/zoocode-paths.ts
36990
+ /**
36991
+ * Zoo Code is a VS Code extension, so its committable command allow/deny lists
36992
+ * are workspace settings rather than files in the `.roo/` agent-asset tree that
36993
+ * the other Zoo Code features write.
36994
+ *
36995
+ * `zoo-code.allowedCommands` / `zoo-code.deniedCommands` are contributed with no
36996
+ * `scope`, which in VS Code means `window` scope — settable in a workspace's
36997
+ * `.vscode/settings.json` — and `ClineProvider.mergeCommandLists()` unions the
36998
+ * workspace values into the effective auto-approval lists.
36999
+ *
37000
+ * The `zoo-code.*` namespace is Zoo-era (the v3.74.0 rebrand); the archived Roo
37001
+ * Code lineage spelled the same settings `roo-cline.*`, so this surface is
37002
+ * deliberately not shared with the `roo` target.
37003
+ *
37004
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
37005
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/webview/ClineProvider.ts
37006
+ */
37007
+ const ZOOCODE_VSCODE_SETTINGS_DIR = ".vscode";
37008
+ const ZOOCODE_VSCODE_SETTINGS_FILE_NAME = "settings.json";
37009
+ const ZOOCODE_ALLOWED_COMMANDS_KEY = "zoo-code.allowedCommands";
37010
+ const ZOOCODE_DENIED_COMMANDS_KEY = "zoo-code.deniedCommands";
37011
+ //#endregion
37012
+ //#region src/features/permissions/zoocode-permissions.ts
37013
+ /**
37014
+ * The canonical category Zoo Code's command lists correspond to. Zoo Code gates
37015
+ * terminal command execution and nothing else through these settings, so only
37016
+ * `bash` maps; `read`/`write`/`edit`/`webfetch` have no workspace-settable
37017
+ * counterpart in the extension's contributions.
37018
+ */
37019
+ const COMMAND_CATEGORY = "bash";
37020
+ function asStringArray(value) {
37021
+ if (!Array.isArray(value)) return [];
37022
+ return value.filter((entry) => typeof entry === "string");
37023
+ }
37024
+ /**
37025
+ * Split one canonical category's rules into Zoo Code's two command lists.
37026
+ *
37027
+ * Zoo Code matches these entries as command **prefixes**: a command runs
37028
+ * without a confirmation prompt when it starts with an `allowedCommands` entry,
37029
+ * and is refused outright when it starts with a `deniedCommands` entry (deny
37030
+ * wins). `ask` is represented by listing the pattern in neither list, which
37031
+ * leaves Zoo Code's default approval prompt in charge.
37032
+ *
37033
+ * Each list is `undefined` when it would be empty, so the key is retracted from
37034
+ * the settings file rather than written as an empty array — an empty
37035
+ * `allowedCommands` and an absent one mean the same thing to Zoo Code, and the
37036
+ * absent form leaves no rulesync residue behind.
37037
+ */
37038
+ function buildCommandLists(rules) {
37039
+ const allowed = [];
37040
+ const denied = [];
37041
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowed.push(pattern);
37042
+ else if (action === "deny") denied.push(pattern);
37043
+ return {
37044
+ allowed: allowed.length > 0 ? allowed : void 0,
37045
+ denied: denied.length > 0 ? denied : void 0
37046
+ };
37047
+ }
37048
+ /**
37049
+ * Permissions generator for Zoo Code.
37050
+ *
37051
+ * Zoo Code has no policy file in its `.roo/` tree: the committable command
37052
+ * allow/deny lists are VS Code workspace settings
37053
+ * (`zoo-code.allowedCommands` / `zoo-code.deniedCommands` in
37054
+ * `.vscode/settings.json`), which `ClineProvider.mergeCommandLists()` unions
37055
+ * into the lists the auto-approval decision reads. That file is a
37056
+ * general-purpose workspace settings file with many unrelated keys, so reads
37057
+ * and writes merge into the existing JSONC (touching only the two managed keys)
37058
+ * and the file is never deleted.
37059
+ *
37060
+ * Only project scope is modeled: VS Code's user-scope `settings.json` lives at
37061
+ * a platform-dependent path outside rulesync's home-relative global model.
37062
+ *
37063
+ * The `roo` target deliberately does not get this adapter. The settings
37064
+ * namespace is Zoo-era (`roo-cline.*` before the v3.74.0 rebrand), and Roo Code
37065
+ * is EOL with its repository archived, so emitting `zoo-code.*` keys for a
37066
+ * `--targets roo` generate would write settings that Roo itself never reads.
37067
+ *
37068
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
37069
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/auto-approval/commands.ts
37070
+ */
37071
+ var ZoocodePermissions = class ZoocodePermissions extends ToolPermissions {
37072
+ constructor(params) {
37073
+ super({
37074
+ ...params,
37075
+ fileContent: params.fileContent ?? "{}"
37076
+ });
37077
+ }
37078
+ /**
37079
+ * `.vscode/settings.json` is a user-managed workspace file with unrelated
37080
+ * settings, so it must not be deleted.
37081
+ */
37082
+ isDeletable() {
37083
+ return false;
37084
+ }
37085
+ static getSettablePaths(_options = {}) {
37086
+ return {
37087
+ relativeDirPath: ZOOCODE_VSCODE_SETTINGS_DIR,
37088
+ relativeFilePath: ZOOCODE_VSCODE_SETTINGS_FILE_NAME
37089
+ };
37090
+ }
37091
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
37092
+ const paths = ZoocodePermissions.getSettablePaths();
37093
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
37094
+ return new ZoocodePermissions({
37095
+ outputRoot,
37096
+ relativeDirPath: paths.relativeDirPath,
37097
+ relativeFilePath: paths.relativeFilePath,
37098
+ fileContent,
37099
+ validate
37100
+ });
37101
+ }
37102
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions }) {
37103
+ const paths = ZoocodePermissions.getSettablePaths();
37104
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
37105
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
37106
+ const rules = rulesyncPermissions.getJson().permission[COMMAND_CATEGORY];
37107
+ const patch = {};
37108
+ if (rules !== void 0) {
37109
+ const { allowed, denied } = buildCommandLists(rules);
37110
+ patch[ZOOCODE_ALLOWED_COMMANDS_KEY] = allowed;
37111
+ patch[ZOOCODE_DENIED_COMMANDS_KEY] = denied;
37112
+ }
37113
+ return new ZoocodePermissions({
37114
+ outputRoot,
37115
+ relativeDirPath: paths.relativeDirPath,
37116
+ relativeFilePath: paths.relativeFilePath,
37117
+ fileContent: applySharedConfigPatch({
37118
+ fileKey: sharedConfigFileKey(paths),
37119
+ feature: "permissions",
37120
+ existingContent,
37121
+ patch,
37122
+ filePath
37123
+ }),
37124
+ validate: true
37125
+ });
37126
+ }
37127
+ toRulesyncPermissions() {
37128
+ let settings;
37129
+ try {
37130
+ settings = parseSharedConfig({
37131
+ format: "jsonc",
37132
+ fileContent: this.getFileContent() || "{}",
37133
+ filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
37134
+ invalidRootPolicy: "error",
37135
+ jsoncParseErrors: "error"
37136
+ });
37137
+ } catch (error) {
37138
+ throw new Error(`Failed to parse Zoo Code VS Code settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
37139
+ }
37140
+ const rules = {};
37141
+ for (const pattern of asStringArray(settings[ZOOCODE_ALLOWED_COMMANDS_KEY])) rules[pattern] = "allow";
37142
+ for (const pattern of asStringArray(settings[ZOOCODE_DENIED_COMMANDS_KEY])) rules[pattern] = "deny";
37143
+ const permission = {};
37144
+ if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
37145
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
37146
+ }
37147
+ validate() {
37148
+ return {
37149
+ success: true,
37150
+ error: null
37151
+ };
37152
+ }
37153
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
37154
+ return new ZoocodePermissions({
37155
+ outputRoot,
37156
+ relativeDirPath,
37157
+ relativeFilePath,
37158
+ fileContent: "{}",
37159
+ validate: false
37160
+ });
37161
+ }
37162
+ };
37163
+ //#endregion
36433
37164
  //#region src/features/permissions/permissions-processor.ts
36434
37165
  const PermissionsProcessorToolTargetSchema = z.enum(permissionsProcessorToolTargetTuple);
36435
37166
  const toolPermissionsFactories = /* @__PURE__ */ new Map([
@@ -36672,6 +37403,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
36672
37403
  supportsGlobal: true,
36673
37404
  supportsImport: true
36674
37405
  }
37406
+ }],
37407
+ ["zoocode", {
37408
+ class: ZoocodePermissions,
37409
+ meta: {
37410
+ supportsProject: true,
37411
+ supportsGlobal: false,
37412
+ supportsImport: true
37413
+ }
36675
37414
  }]
36676
37415
  ]);
36677
37416
  var PermissionsProcessor = class extends FeatureProcessor {
@@ -52051,11 +52790,12 @@ var RooRule = class RooRule extends ToolRule {
52051
52790
  static getSettablePaths(_options = {}) {
52052
52791
  return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
52053
52792
  }
52054
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
52055
- const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().nonRoot.relativeDirPath, relativeFilePath));
52793
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
52794
+ const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
52795
+ const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
52056
52796
  return new RooRule({
52057
52797
  outputRoot,
52058
- relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
52798
+ relativeDirPath,
52059
52799
  relativeFilePath,
52060
52800
  fileContent,
52061
52801
  validate,
@@ -52063,12 +52803,16 @@ var RooRule = class RooRule extends ToolRule {
52063
52803
  });
52064
52804
  }
52065
52805
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
52066
- return new RooRule(this.buildToolRuleParamsDefault({
52806
+ const params = this.buildToolRuleParamsDefault({
52067
52807
  outputRoot,
52068
52808
  rulesyncRule,
52069
52809
  validate,
52070
52810
  nonRootPath: this.getSettablePaths().nonRoot
52071
- }));
52811
+ });
52812
+ const mode = rulesyncRule.getFrontmatter().roo?.mode;
52813
+ if (!params.root && mode !== void 0 && mode !== "") if (!ROO_MODE_SLUG_PATTERN.test(mode)) warnWithFallback(void 0, `Ignoring roo.mode "${mode}" on ${rulesyncRule.getRelativeFilePath()}: a mode slug may contain only letters, digits and hyphens. Writing the rule to ${params.relativeDirPath} instead.`);
52814
+ else params.relativeDirPath = join(dirname(params.relativeDirPath), rooModeRulesDirName(mode));
52815
+ return new RooRule(params);
52072
52816
  }
52073
52817
  /**
52074
52818
  * Extract mode slug from file path for mode-specific rules
@@ -52080,8 +52824,51 @@ var RooRule = class RooRule extends ToolRule {
52080
52824
  const singleFileMatch = filePath.match(/\.(roo|cline)rules-([a-zA-Z0-9-]+)$/);
52081
52825
  if (singleFileMatch) return singleFileMatch[2];
52082
52826
  }
52827
+ /**
52828
+ * The mode slug of a `.roo/rules-{mode}/` directory, or `undefined` for the
52829
+ * generic `.roo/rules/` directory and anything else. Path-shaped input is
52830
+ * rejected by the slug pattern, so a crafted directory name cannot be lifted
52831
+ * back into frontmatter.
52832
+ */
52833
+ static extractModeFromDirPath(relativeDirPath) {
52834
+ for (const segment of toPosixPath(relativeDirPath).split("/")) {
52835
+ if (!segment.startsWith("rules-")) continue;
52836
+ const mode = segment.slice(6);
52837
+ if (ROO_MODE_SLUG_PATTERN.test(mode)) return mode;
52838
+ }
52839
+ }
52083
52840
  toRulesyncRule() {
52084
- return this.toRulesyncRuleDefault();
52841
+ const mode = RooRule.extractModeFromDirPath(this.getRelativeDirPath());
52842
+ if (mode === void 0) return this.toRulesyncRuleDefault();
52843
+ const baseName = this.getRelativeFilePath().replace(/\.md$/, "");
52844
+ const suffix = `-${mode}`;
52845
+ const importedName = baseName.endsWith(suffix) ? baseName : `${baseName}${suffix}`;
52846
+ return new RulesyncRule({
52847
+ outputRoot: this.getOutputRoot(),
52848
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
52849
+ relativeFilePath: `${importedName}.md`,
52850
+ frontmatter: {
52851
+ root: false,
52852
+ targets: [this.constructor.getToolTargetName()],
52853
+ description: this.description,
52854
+ globs: this.globs ?? [],
52855
+ roo: { mode }
52856
+ },
52857
+ body: this.getFileContent(),
52858
+ validate: true
52859
+ });
52860
+ }
52861
+ /**
52862
+ * Mode-specific rule directories (`.roo/rules-{mode}/`), which Roo/Zoo Code
52863
+ * load instead of `.roo/rules/` while that mode is active. Import-only: the
52864
+ * generic directory is the only one the deletion sweep enumerates, because a
52865
+ * `rules-*` glob would also match mode rules a user wrote by hand.
52866
+ */
52867
+ static getNestedFilePatterns({ outputRoot }) {
52868
+ return {
52869
+ include: [`${toPosixPath(outputRoot)}/${toPosixPath(ROO_DIR)}/rules-*/**/*.md`],
52870
+ ignore: []
52871
+ };
52085
52872
  }
52086
52873
  validate() {
52087
52874
  return {
@@ -52102,10 +52889,18 @@ var RooRule = class RooRule extends ToolRule {
52102
52889
  static isTargetedByRulesyncRule(rulesyncRule) {
52103
52890
  return this.isTargetedByRulesyncRuleDefault({
52104
52891
  rulesyncRule,
52105
- toolTarget: "roo"
52892
+ toolTarget: this.getToolTargetName()
52106
52893
  });
52107
52894
  }
52108
52895
  /**
52896
+ * The tool target this class imports as. `ZoocodeRule` narrows the same
52897
+ * `.roo/` adapters to the `zoocode` target, so the name has to come from the
52898
+ * class rather than a literal.
52899
+ */
52900
+ static getToolTargetName() {
52901
+ return "roo";
52902
+ }
52903
+ /**
52109
52904
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
52110
52905
  * at the project root, not under `.roo/` (mirrors rovodev).
52111
52906
  */
@@ -52638,11 +53433,8 @@ var ZedRule = class ZedRule extends ToolRule {
52638
53433
  * @see https://docs.zoocode.dev
52639
53434
  */
52640
53435
  var ZoocodeRule = class extends RooRule {
52641
- static isTargetedByRulesyncRule(rulesyncRule) {
52642
- return this.isTargetedByRulesyncRuleDefault({
52643
- rulesyncRule,
52644
- toolTarget: "zoocode"
52645
- });
53436
+ static getToolTargetName() {
53437
+ return "zoocode";
52646
53438
  }
52647
53439
  };
52648
53440
  //#endregion
@@ -55609,6 +56401,6 @@ async function importChecksCore(params) {
55609
56401
  return writtenCount;
55610
56402
  }
55611
56403
  //#endregion
55612
- export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
56404
+ export { ConsoleLogger as $, RULESYNC_PERMISSIONS_FILE_NAME as $t, RulesyncRule as A, ALL_TOOL_TARGETS as At, RulesyncCommandFrontmatterSchema as B, RULESYNC_CONFIG_SCHEMA_URL as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, removeFileStrict as Ct, RulesyncSubagentFrontmatterSchema as D, toPosixPath as Dt, RulesyncSubagent as E, runWithDirectoryRollback as Et, RulesyncHooks as F, RULESYNC_AIIGNORE_FILE_NAME as Ft, SHARED_USER_MANAGED_CONFIG_PATHS as G, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Gt, RulesyncCheckFrontmatterSchema as H, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Ht, getRulesyncSourceCandidates as I, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as It, CONFLICTING_TARGET_PAIRS as J, RULESYNC_MCP_FILE_NAME as Jt, SKILL_FILE_NAME as K, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Kt, resolveRulesyncSourceWritePath as L, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Lt, RulesyncPermissions as M, PACKAGING_TOOL_TARGETS as Mt, RulesyncMcp as N, ToolTargetSchema as Nt, RulesyncSkill as O, writeFileBuffer as Ot, RulesyncIgnore as P, MAX_FILE_SIZE as Pt, findControlCharacter as Q, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Qt, parseJsonc as R, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Rt, ChecksProcessor as S, removeFile as St, getLocalSkillDirNames as T, resolvePath as Tt, stringifyFrontmatter as U, RULESYNC_HOOKS_FILE_NAME as Ut, RulesyncCheck as V, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Vt, loadYaml as W, RULESYNC_HOOKS_LEGACY_FILE_NAME as Wt, GITIGNORE_DESTINATION_KEY as X, RULESYNC_MCP_RELATIVE_FILE_PATH as Xt, ConfigFileSchema as Y, RULESYNC_MCP_LEGACY_FILE_NAME as Yt, SourceEntrySchema as Z, RULESYNC_MCP_SCHEMA_URL as Zt, CLAUDECODE_DIR as _, listDirectoryFiles as _t, convertFromTool as a, RULESYNC_SKILLS_RELATIVE_DIR_PATH as an, assertDirectoryIfExists as at, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as b, removeDirectory as bt, SubagentsProcessor as c, ALL_FEATURES as cn, checkPathTraversal as ct, McpProcessor as d, formatError as dn, ensureDir as dt, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as en, JsonLogger as et, IgnoreProcessor as f, fileExists as ft, QWENCODE_LOCAL_RULE_FILE_NAME as g, isSymlink as gt, QWENCODE_DIR as h, getHomeDirectory as ht, getProcessorRegistryEntry as i, RULESYNC_RULES_RELATIVE_DIR_PATH as in, ErrorCodes as it, RulesyncRuleFrontmatterSchema as j, ALL_TOOL_TARGETS_WITH_WILDCARD as jt, RulesyncSkillFrontmatterSchema as k, writeFileContent as kt, SkillsProcessor as l, ALL_FEATURES_WITH_WILDCARD as ln, createTempDirectory as lt, CommandsProcessor as m, getFileSize as mt, checkRulesyncDirExists as n, RULESYNC_PERMISSIONS_SCHEMA_URL as nn, warnOnConflictingFlags as nt, isPackagingToolTarget as o, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as on, assertTreeContainsNoSymlinks as ot, HooksProcessor as p, findFilesByGlobs as pt, ConfigResolver as q, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as qt, generate as r, RULESYNC_RELATIVE_DIR_PATH as rn, CLIError as rt, RulesProcessor as s, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as sn, assertWritablePathInsideRoot as st, importFromTool as t, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as tn, fallbackLogger as tt, stripControlCharacters as u, DEPRECATED_FEATURE_REPLACEMENTS as un, directoryExists as ut, CLAUDECODE_LOCAL_RULE_FILE_NAME as v, readFileContent as vt, CODEXCLI_DIR as w, removeTempDirectory as wt, CLAUDECODE_SKILLS_DIR_PATH as x, removeDirectoryStrict as xt, CLAUDECODE_MEMORIES_DIR_NAME as y, readFileContentOrNull as yt, RulesyncCommand as z, RULESYNC_CONFIG_RELATIVE_FILE_PATH as zt };
55613
56405
 
55614
- //# sourceMappingURL=import-Dswkc7Ub.js.map
56406
+ //# sourceMappingURL=import-BPTCMtUS.js.map