rulesync 37.1.0 → 38.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
457
457
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
458
458
  //#endregion
459
459
  //#region src/constants/rulesync-paths.ts
460
- const { join: join$423 } = node_path.posix;
460
+ const { join: join$426 } = node_path.posix;
461
461
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
462
462
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
463
463
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
464
464
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
465
465
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
466
466
  const RULES_FEATURE_SUBDIR = "rules";
467
- const CURATED_RULES_FEATURE_SUBDIR = join$423(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$426(RULES_FEATURE_SUBDIR, ".curated");
468
468
  const COMMANDS_FEATURE_SUBDIR = "commands";
469
469
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
470
470
  const CHECKS_FEATURE_SUBDIR = "checks";
471
471
  const SKILLS_FEATURE_SUBDIR = "skills";
472
- const CURATED_SKILLS_FEATURE_SUBDIR = join$423(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$423(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$426(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$426(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
484
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
485
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
486
486
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
487
487
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
488
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
490
490
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
491
491
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
492
492
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -652,6 +652,7 @@ const mcpProcessorToolTargetTuple = [
652
652
  "musecode",
653
653
  "mimocode",
654
654
  "omp",
655
+ "openclaw",
655
656
  "opencode",
656
657
  "pi",
657
658
  "pool",
@@ -704,6 +705,7 @@ const commandsProcessorToolTargetTuple = [
704
705
  "kiro",
705
706
  "kiro-cli",
706
707
  "kiro-ide",
708
+ "lettacode",
707
709
  "mimocode",
708
710
  "omp",
709
711
  "opencode",
@@ -931,6 +933,7 @@ const permissionsProcessorToolTargetTuple = [
931
933
  "rovodev",
932
934
  "tabnine",
933
935
  "takt",
936
+ "trae",
934
937
  "vibe",
935
938
  "warp",
936
939
  "warpcli",
@@ -4764,10 +4767,13 @@ const CLAUDE_HOOK_EVENTS = [
4764
4767
  * eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
4765
4768
  * `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
4766
4769
  * `PostCompaction` (fires after context compaction; added in the Devin CLI
4767
- * stable changelog — https://docs.devin.ai/cli/changelog/stable). The
4768
- * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
4769
- * a `matcher` (regex against `tool_name`); the session/turn/compaction events
4770
- * do not.
4770
+ * stable changelog — https://docs.devin.ai/cli/changelog/stable). CLI
4771
+ * v3000.11.92 added `PreCompact` and `PostCompact`, which run before and after
4772
+ * compaction with `manual` / `auto` trigger matchers; `PostCompaction` stays
4773
+ * supported. The tool/permission events (`PreToolUse`/`PostToolUse`/
4774
+ * `PermissionRequest`) carry a `matcher` (regex against `tool_name`) and
4775
+ * `PreCompact` a trigger matcher; the other session/turn/compaction events do
4776
+ * not.
4771
4777
  *
4772
4778
  * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
4773
4779
  * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
@@ -4783,6 +4789,7 @@ const DEVIN_HOOK_EVENTS = [
4783
4789
  "beforeSubmitPrompt",
4784
4790
  "stop",
4785
4791
  "permissionRequest",
4792
+ "preCompact",
4786
4793
  "postCompact"
4787
4794
  ];
4788
4795
  /**
@@ -6166,12 +6173,17 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6166
6173
  beforeSubmitPrompt: "UserPromptSubmit",
6167
6174
  stop: "Stop",
6168
6175
  permissionRequest: "PermissionRequest",
6176
+ preCompact: "PreCompact",
6169
6177
  postCompact: "PostCompaction"
6170
6178
  };
6171
6179
  /**
6172
- * Map Devin Local PascalCase event names to canonical camelCase.
6180
+ * Map Devin Local PascalCase event names to canonical camelCase. `PostCompact`
6181
+ * (CLI v3000.11.92) is accepted on import as an alias of `PostCompaction`.
6173
6182
  */
6174
- const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
6183
+ const DEVIN_TO_CANONICAL_EVENT_NAMES = {
6184
+ ...Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k])),
6185
+ PostCompact: "postCompact"
6186
+ };
6175
6187
  /**
6176
6188
  * Map canonical camelCase event names to AugmentCode PascalCase.
6177
6189
  * Auggie reuses the same PascalCase names as Claude for the events it supports.
@@ -7104,6 +7116,17 @@ const CodexcliMcpToolConfigSchema = zod_mini.z.looseObject({
7104
7116
  function isCodexcliToolConfig(value) {
7105
7117
  return CodexcliMcpToolConfigSchema.safeParse(value).success;
7106
7118
  }
7119
+ const CrushChannelReplyRouteSchema = zod_mini.z.looseObject({
7120
+ tool: zod_mini.z.string(),
7121
+ target_param: zod_mini.z.optional(zod_mini.z.string()),
7122
+ target_meta: zod_mini.z.optional(zod_mini.z.string())
7123
+ });
7124
+ const CrushChannelReplySchema = zod_mini.z.looseObject({
7125
+ user: zod_mini.z.optional(CrushChannelReplyRouteSchema),
7126
+ group: zod_mini.z.optional(CrushChannelReplyRouteSchema),
7127
+ message_param: zod_mini.z.optional(zod_mini.z.string()),
7128
+ suppress_tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
7129
+ });
7107
7130
  const McpServerSchema = zod_mini.z.looseObject({
7108
7131
  type: zod_mini.z.optional(zod_mini.z.enum([
7109
7132
  "local",
@@ -7145,6 +7168,9 @@ const McpServerSchema = zod_mini.z.looseObject({
7145
7168
  crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
7146
7169
  crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
7147
7170
  crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
7171
+ crushChannelEnabled: zod_mini.z.optional(zod_mini.z.boolean()),
7172
+ crushChannelReply: zod_mini.z.optional(CrushChannelReplySchema),
7173
+ zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({ type: zod_mini.z.string() })),
7148
7174
  poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
7149
7175
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
7150
7176
  /**
@@ -7256,6 +7282,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
7256
7282
  kiro: zod_mini.z.optional(toolScopedMcpSchema),
7257
7283
  mimocode: zod_mini.z.optional(toolScopedMcpSchema),
7258
7284
  omp: zod_mini.z.optional(toolScopedMcpSchema),
7285
+ openclaw: zod_mini.z.optional(toolScopedMcpSchema),
7259
7286
  opencode: zod_mini.z.optional(toolScopedMcpSchema),
7260
7287
  pi: zod_mini.z.optional(toolScopedMcpSchema),
7261
7288
  qoder: zod_mini.z.optional(toolScopedMcpSchema),
@@ -7603,11 +7630,16 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
7603
7630
  "crushOauthClientSecret",
7604
7631
  "crushOauthCallbackPort",
7605
7632
  "crushSessionless",
7633
+ "crushChannelEnabled",
7634
+ "crushChannelReply",
7606
7635
  "poolAllow",
7636
+ "zcodeOauth",
7607
7637
  "oauth_client_id",
7608
7638
  "oauth_client_secret",
7609
7639
  "oauth_callback_port",
7610
7640
  "sessionless",
7641
+ "channel_enabled",
7642
+ "channel_reply",
7611
7643
  "enabled"
7612
7644
  ])];
7613
7645
  }));
@@ -8343,8 +8375,12 @@ const TabninePermissionsOverrideSchema = zod_mini.z.looseObject({
8343
8375
  * are merged into the matching `reasonix.toml` table and emitted only for
8344
8376
  * Reasonix, while the shared `permission` block continues to drive
8345
8377
  * `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
8346
- * Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
8347
- * re-extracted from `[agent]` on import — other `agent` keys authored here reach
8378
+ * The user-only `[sandbox]` keys (`allow_devices`, `egress_unlisted`,
8379
+ * `host_authorities`) are written only by `--global` generate: Reasonix replaces
8380
+ * a project value with the user's (`narrowSandbox` in `project_scope.go`).
8381
+ * Note: the whole `[sandbox]` table round-trips (minus the user-only keys of a
8382
+ * project file), but only the plan-mode keys are re-extracted from `[agent]` on
8383
+ * import — other `agent` keys authored here reach
8348
8384
  * `reasonix.toml` on generate but are not re-extracted back into the override.
8349
8385
  *
8350
8386
  * @example
@@ -8376,14 +8412,52 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8376
8412
  * output-style` rule option) or a `plugin-name:style-name` reference. The
8377
8413
  * shared `permission` block continues to drive `permissions.allow`/`ask`/`deny`.
8378
8414
  *
8415
+ * The other documented settings with no canonical category are authored here
8416
+ * too and merged into the same file, key by key, over what the file already
8417
+ * holds: `general.defaultPermissionMode`, `tools.core` / `tools.exclude`, the
8418
+ * MCP approval and loading keys under `mcp` (`allowed`, `excluded`,
8419
+ * `enableAllProjectMcpServers`, `enabledProjectMcpServers`, `lazyLoad`),
8420
+ * `agents.overrides`, the top-level `agentsMdExcludes` and the top-level
8421
+ * `autoMode` classifier guidance (`allow` / `soft_deny` / `environment`).
8422
+ * Qoder reads `autoMode` from the user settings only, so it is written by
8423
+ * `--global` generate alone. Only the keys listed here are written; any other
8424
+ * key of these groups or of `autoMode` (`tools.shell.pager`,
8425
+ * `general.preferredEditor`, ...) is skipped with a warning. Each authored key
8426
+ * is laid over what the file holds; removing it from the override leaves the
8427
+ * previously written value in place.
8428
+ *
8379
8429
  * @see https://docs.qoder.com/cli/output-styles
8380
8430
  * @see https://docs.qoder.com/cli/settings-reference
8431
+ * @see https://docs.qoder.com/en/cli/permissions
8381
8432
  * @example
8382
8433
  * { "outputStyle": "concise-review" }
8434
+ * @example
8435
+ * { "mcp": { "enabledProjectMcpServers": ["playwright"], "lazyLoad": true },
8436
+ * "tools": { "exclude": ["WebSearch"] },
8437
+ * "autoMode": { "environment": ["This is a pnpm monorepo"] } }
8383
8438
  */
8384
8439
  const QoderPermissionsOverrideSchema = zod_mini.z.looseObject({
8385
8440
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8386
- outputStyle: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1)))
8441
+ outputStyle: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1))),
8442
+ general: zod_mini.z.optional(zod_mini.z.looseObject({ defaultPermissionMode: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1))) })),
8443
+ tools: zod_mini.z.optional(zod_mini.z.looseObject({
8444
+ core: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8445
+ exclude: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8446
+ })),
8447
+ mcp: zod_mini.z.optional(zod_mini.z.looseObject({
8448
+ allowed: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8449
+ excluded: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8450
+ enableAllProjectMcpServers: zod_mini.z.optional(zod_mini.z.boolean()),
8451
+ enabledProjectMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8452
+ lazyLoad: zod_mini.z.optional(zod_mini.z.boolean())
8453
+ })),
8454
+ agents: zod_mini.z.optional(zod_mini.z.looseObject({ overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({}))) })),
8455
+ autoMode: zod_mini.z.optional(zod_mini.z.looseObject({
8456
+ allow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8457
+ soft_deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8458
+ environment: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8459
+ })),
8460
+ agentsMdExcludes: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8387
8461
  });
8388
8462
  /**
8389
8463
  * One native Factory Droid permission rule. `match.prefix` positions are either
@@ -9171,14 +9245,40 @@ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
9171
9245
  *
9172
9246
  * @example
9173
9247
  * { "sandbox": { "allowed_domains": ["github.com"], "network_mode": "limited" } }
9248
+ * The same override carries the repo-level plugin governance lists Devin reads
9249
+ * from a checkout's `.devin/config.json`: `requiredPlugins` (plugin sources
9250
+ * installed automatically), `optionalPlugins` (endorsed sources that carve an
9251
+ * exception out of a forbidden entry; never installed) and `forbiddenPlugins`
9252
+ * (plugin identities or `*` globs that may not load). A source is a string
9253
+ * (`"owner/repo"` or a git URL) or an object such as
9254
+ * `{ "source": "github", "repo": "owner/repo" }`. They are a repo governance
9255
+ * level, so they are emitted at project scope only.
9256
+ *
9174
9257
  * @example
9175
9258
  * { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
9259
+ * @example
9260
+ * { "forbiddenPlugins": ["*"], "optionalPlugins": ["acme/approved"] }
9176
9261
  * @see https://docs.devin.ai/cli/sandbox
9177
9262
  * @see https://docs.devin.ai/cli/reference/configuration/config-file
9263
+ * @see https://docs.devin.ai/cli/extensibility/plugins/overview
9264
+ */
9265
+ const DevinPluginSourceListSchema = zod_mini.z.array(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})]));
9266
+ /**
9267
+ * Shapes of the Devin repo-level plugin governance lists, keyed by their
9268
+ * `.devin/config.json` name. Shared by the override schema and the import,
9269
+ * which drops a list that would not pass it.
9178
9270
  */
9271
+ const DEVIN_PLUGIN_GOVERNANCE_SCHEMAS = {
9272
+ requiredPlugins: DevinPluginSourceListSchema,
9273
+ optionalPlugins: DevinPluginSourceListSchema,
9274
+ forbiddenPlugins: zod_mini.z.array(zod_mini.z.string())
9275
+ };
9179
9276
  const DevinPermissionsOverrideSchema = zod_mini.z.looseObject({
9180
9277
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
9181
- sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
9278
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
9279
+ requiredPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.requiredPlugins),
9280
+ optionalPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.optionalPlugins),
9281
+ forbiddenPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins)
9182
9282
  });
9183
9283
  /**
9184
9284
  * Permissions configuration.
@@ -9252,6 +9352,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
9252
9352
  "kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
9253
9353
  roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9254
9354
  rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9355
+ trae: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9255
9356
  zoocode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
9256
9357
  });
9257
9358
  /**
@@ -11110,6 +11211,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
11110
11211
  compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
11111
11212
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
11112
11213
  })),
11214
+ musecode: zod_mini.z.optional(zod_mini.z.looseObject({
11215
+ license: zod_mini.z.optional(zod_mini.z.string()),
11216
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
11217
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
11218
+ })),
11113
11219
  zcode: zod_mini.z.optional(zod_mini.z.looseObject({
11114
11220
  when_to_use: zod_mini.z.optional(zod_mini.z.string()),
11115
11221
  license: zod_mini.z.optional(zod_mini.z.string()),
@@ -11540,8 +11646,20 @@ const GITLABDUO_MR_REVIEW_INSTRUCTIONS_FILE_NAME = "mr-review-instructions.yaml"
11540
11646
  const GITLABDUO_COMMANDS_DIR = (0, node_path.join)(".agents", "commands");
11541
11647
  /** User custom slash commands. */
11542
11648
  const GITLABDUO_GLOBAL_COMMANDS_DIR = (0, node_path.join)(GITLABDUO_DIR, "commands");
11543
- /** Project skills live in a non-hidden `skills/` directory at the project root. */
11544
- const GITLABDUO_SKILLS_DIR = "skills";
11649
+ /**
11650
+ * Project skills live in the cross-tool `.agents/skills/` directory, which
11651
+ * GitLab Duo reads alongside a non-hidden `skills/` directory at the project
11652
+ * root; when both hold a skill of the same name, `.agents/skills/` wins.
11653
+ *
11654
+ * @see https://docs.gitlab.com/user/duo_agent_platform/customize/agent_skills/
11655
+ */
11656
+ const GITLABDUO_SKILLS_DIR = (0, node_path.join)(".agents", "skills");
11657
+ /**
11658
+ * The project-root `skills/` directory GitLab Duo also reads. Earlier rulesync
11659
+ * versions wrote project skills there; it is now an import-only root, since it
11660
+ * routinely holds hand-authored skills rulesync must not manage.
11661
+ */
11662
+ const GITLABDUO_LEGACY_SKILLS_DIR = "skills";
11545
11663
  /** User skills (requires `--enable-global-skills` / `GITLAB_ENABLE_GLOBAL_SKILLS`). */
11546
11664
  const GITLABDUO_GLOBAL_SKILLS_DIR = (0, node_path.join)(GITLABDUO_DIR, "skills");
11547
11665
  //#endregion
@@ -11703,6 +11821,239 @@ const CODEXCLI_OVERRIDE_KEYS = [
11703
11821
  "approvals_reviewer",
11704
11822
  "tui"
11705
11823
  ];
11824
+ const SINGLE_CHAR_ESCAPES = {
11825
+ "'": "'",
11826
+ "\"": "\"",
11827
+ "\\": "\\",
11828
+ b: "\b",
11829
+ f: "\f",
11830
+ n: "\n",
11831
+ r: "\r",
11832
+ t: " ",
11833
+ v: "\v"
11834
+ };
11835
+ const LINE_TERMINATORS = /* @__PURE__ */ new Set([
11836
+ "\n",
11837
+ "\r",
11838
+ "\u2028",
11839
+ "\u2029"
11840
+ ]);
11841
+ const ID_START = /[\p{L}\p{Nl}$_]/u;
11842
+ const ID_CONTINUE = /[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$_\u200C\u200D]/u;
11843
+ const WHITESPACE$1 = /[\t\n\v\f\r \u00A0\u2028\u2029\uFEFF\p{Zs}]/u;
11844
+ var Json5Reader = class {
11845
+ text;
11846
+ pos = 0;
11847
+ constructor(text) {
11848
+ this.text = text;
11849
+ }
11850
+ parse() {
11851
+ this.skipTrivia();
11852
+ const value = this.readValue(0);
11853
+ this.skipTrivia();
11854
+ if (this.pos < this.text.length) this.fail(`unexpected ${this.describe()} after the end of the document`);
11855
+ return value;
11856
+ }
11857
+ fail(message) {
11858
+ let line = 1;
11859
+ let column = 1;
11860
+ for (let i = 0; i < this.pos && i < this.text.length; i++) if (this.text[i] === "\n") {
11861
+ line++;
11862
+ column = 1;
11863
+ } else column++;
11864
+ throw new SyntaxError(`JSON5: ${message} at line ${line}, column ${column}`);
11865
+ }
11866
+ describe() {
11867
+ const char = this.text[this.pos];
11868
+ return char === void 0 ? "end of input" : `character ${JSON.stringify(char)}`;
11869
+ }
11870
+ peek(offset = 0) {
11871
+ return this.text[this.pos + offset];
11872
+ }
11873
+ skipTrivia() {
11874
+ while (this.pos < this.text.length) {
11875
+ const char = this.text[this.pos];
11876
+ if (WHITESPACE$1.test(char)) this.pos++;
11877
+ else if (char === "/" && this.peek(1) === "/") {
11878
+ this.pos += 2;
11879
+ while (this.pos < this.text.length && !LINE_TERMINATORS.has(this.text[this.pos])) this.pos++;
11880
+ } else if (char === "/" && this.peek(1) === "*") {
11881
+ const end = this.text.indexOf("*/", this.pos + 2);
11882
+ if (end === -1) this.fail("unterminated block comment");
11883
+ this.pos = end + 2;
11884
+ } else return;
11885
+ }
11886
+ }
11887
+ readValue(depth) {
11888
+ const char = this.peek();
11889
+ if (char === "{" || char === "[") {
11890
+ if (depth >= 256) this.fail(`document nests deeper than 256 levels`);
11891
+ return char === "{" ? this.readObject(depth + 1) : this.readArray(depth + 1);
11892
+ }
11893
+ if (char === "\"" || char === "'") return this.readString();
11894
+ if (char !== void 0 && /[0-9+\-.IN]/.test(char)) return this.readNumber();
11895
+ for (const [literal, value] of [
11896
+ ["true", true],
11897
+ ["false", false],
11898
+ ["null", null]
11899
+ ]) if (this.text.startsWith(literal, this.pos) && !this.isIdContinueAt(literal.length)) {
11900
+ this.pos += literal.length;
11901
+ return value;
11902
+ }
11903
+ return this.fail(`unexpected ${this.describe()}`);
11904
+ }
11905
+ isIdContinueAt(offset) {
11906
+ const char = this.peek(offset);
11907
+ return char !== void 0 && (ID_CONTINUE.test(char) || char === "\\");
11908
+ }
11909
+ readObject(depth) {
11910
+ const result = {};
11911
+ this.pos++;
11912
+ this.skipTrivia();
11913
+ while (this.peek() !== "}") {
11914
+ const key = this.readKey();
11915
+ this.skipTrivia();
11916
+ if (this.peek() !== ":") this.fail(`expected ':' after the key ${JSON.stringify(key)}`);
11917
+ this.pos++;
11918
+ this.skipTrivia();
11919
+ const value = this.readValue(depth);
11920
+ Object.defineProperty(result, key, {
11921
+ value,
11922
+ enumerable: true,
11923
+ writable: true,
11924
+ configurable: true
11925
+ });
11926
+ this.skipTrivia();
11927
+ if (this.peek() === ",") {
11928
+ this.pos++;
11929
+ this.skipTrivia();
11930
+ continue;
11931
+ }
11932
+ if (this.peek() !== "}") this.fail(`expected ',' or '}' but found ${this.describe()}`);
11933
+ }
11934
+ this.pos++;
11935
+ return result;
11936
+ }
11937
+ readArray(depth) {
11938
+ const result = [];
11939
+ this.pos++;
11940
+ this.skipTrivia();
11941
+ while (this.peek() !== "]") {
11942
+ result.push(this.readValue(depth));
11943
+ this.skipTrivia();
11944
+ if (this.peek() === ",") {
11945
+ this.pos++;
11946
+ this.skipTrivia();
11947
+ continue;
11948
+ }
11949
+ if (this.peek() !== "]") this.fail(`expected ',' or ']' but found ${this.describe()}`);
11950
+ }
11951
+ this.pos++;
11952
+ return result;
11953
+ }
11954
+ readKey() {
11955
+ const char = this.peek();
11956
+ if (char === "\"" || char === "'") return this.readString();
11957
+ let key = "";
11958
+ while (this.pos < this.text.length) {
11959
+ const start = this.pos;
11960
+ let next = this.peek();
11961
+ if (next === "\\") {
11962
+ if (this.peek(1) !== "u") this.fail("expected a \\u escape in an identifier key");
11963
+ this.pos += 2;
11964
+ next = this.readHex(4);
11965
+ } else this.pos++;
11966
+ if (!(key === "" ? ID_START.test(next) : ID_CONTINUE.test(next))) {
11967
+ this.pos = start;
11968
+ break;
11969
+ }
11970
+ key += next;
11971
+ }
11972
+ if (key === "") this.fail(`expected a key but found ${this.describe()}`);
11973
+ return key;
11974
+ }
11975
+ /** Reads `length` hex digits at the cursor and returns the character they encode. */
11976
+ readHex(length) {
11977
+ const digits = this.text.slice(this.pos, this.pos + length);
11978
+ if (digits.length !== length || !/^[0-9a-fA-F]+$/.test(digits)) this.fail(`expected ${length} hexadecimal digits`);
11979
+ this.pos += length;
11980
+ return String.fromCharCode(Number.parseInt(digits, 16));
11981
+ }
11982
+ readString() {
11983
+ const quote = this.peek();
11984
+ this.pos++;
11985
+ let result = "";
11986
+ while (true) {
11987
+ const char = this.peek();
11988
+ if (char === void 0) this.fail("unterminated string");
11989
+ if (char === quote) {
11990
+ this.pos++;
11991
+ return result;
11992
+ }
11993
+ if (char === "\n" || char === "\r") this.fail("unescaped line break in a string");
11994
+ if (char !== "\\") {
11995
+ result += char;
11996
+ this.pos++;
11997
+ continue;
11998
+ }
11999
+ this.pos++;
12000
+ const escape = this.peek();
12001
+ if (escape === void 0) this.fail("unterminated string");
12002
+ if (escape === "\r") this.pos += this.peek(1) === "\n" ? 2 : 1;
12003
+ else if (LINE_TERMINATORS.has(escape)) this.pos++;
12004
+ else if (escape === "x") {
12005
+ this.pos++;
12006
+ result += this.readHex(2);
12007
+ } else if (escape === "u") {
12008
+ this.pos++;
12009
+ result += this.readHex(4);
12010
+ } else if (escape === "0") {
12011
+ if (/[0-9]/.test(this.peek(1) ?? "")) this.fail("octal escapes are not allowed");
12012
+ result += "\0";
12013
+ this.pos++;
12014
+ } else if (/[1-9]/.test(escape)) this.fail("octal escapes are not allowed");
12015
+ else {
12016
+ result += SINGLE_CHAR_ESCAPES[escape] ?? escape;
12017
+ this.pos++;
12018
+ }
12019
+ }
12020
+ }
12021
+ readNumber() {
12022
+ const start = this.pos;
12023
+ let sign = 1;
12024
+ if (this.peek() === "+" || this.peek() === "-") {
12025
+ sign = this.peek() === "-" ? -1 : 1;
12026
+ this.pos++;
12027
+ }
12028
+ for (const [literal, value] of [["Infinity", Infinity], ["NaN", NaN]]) if (this.text.startsWith(literal, this.pos)) {
12029
+ if (this.isIdContinueAt(literal.length)) break;
12030
+ this.pos += literal.length;
12031
+ return sign * value;
12032
+ }
12033
+ if (this.peek() === "0" && (this.peek(1) === "x" || this.peek(1) === "X")) {
12034
+ this.pos += 2;
12035
+ const digits = /^[0-9a-fA-F]+/.exec(this.text.slice(this.pos))?.[0];
12036
+ if (digits === void 0) this.fail("expected hexadecimal digits");
12037
+ this.pos += digits.length;
12038
+ return sign * Number.parseInt(digits, 16);
12039
+ }
12040
+ const match = /^(?:(?:0|[1-9][0-9]*)(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.pos));
12041
+ if (match === null) {
12042
+ this.pos = start;
12043
+ this.fail(`invalid number`);
12044
+ }
12045
+ this.pos += match[0].length;
12046
+ if (this.isIdContinueAt(0)) this.fail(`invalid number`);
12047
+ return sign * Number(match[0]);
12048
+ }
12049
+ };
12050
+ /**
12051
+ * Parse a JSON5 document. Throws a `SyntaxError` naming the line and column of
12052
+ * the first problem.
12053
+ */
12054
+ function parseJson5(text) {
12055
+ return new Json5Reader(text).parse();
12056
+ }
11706
12057
  //#endregion
11707
12058
  //#region src/utils/quote-value.ts
11708
12059
  /**
@@ -11852,6 +12203,7 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
11852
12203
  if (format === "yaml") parsed = loadYaml(fileContent);
11853
12204
  else if (format === "toml") parsed = (0, smol_toml.parse)(fileContent);
11854
12205
  else if (format === "json") parsed = JSON.parse(fileContent);
12206
+ else if (format === "json5") parsed = parseJson5(fileContent);
11855
12207
  else if (jsoncParseErrors === "error") {
11856
12208
  const errors = [];
11857
12209
  parsed = (0, jsonc_parser.parse)(fileContent, errors, { allowTrailingComma: true });
@@ -12449,8 +12801,12 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
12449
12801
  format,
12450
12802
  document
12451
12803
  });
12452
- if (format !== "jsonc" || existingContent.trim() === "") return whole;
12804
+ if (format !== "jsonc" && format !== "json5" || existingContent.trim() === "") return whole;
12453
12805
  const wholeBecause = (reason) => {
12806
+ if (format === "json5") throw new Json5EditRefusedError({
12807
+ filePath,
12808
+ reason
12809
+ });
12454
12810
  warnAboutWholeJsoncRewrite({
12455
12811
  filePath,
12456
12812
  reason,
@@ -12461,7 +12817,7 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
12461
12817
  try {
12462
12818
  const errors = [];
12463
12819
  const root = (0, jsonc_parser.parseTree)(existingContent, errors, { allowTrailingComma: true });
12464
- if (root === void 0 || errors.length > 0) return wholeBecause("it could not be parsed");
12820
+ if (root === void 0 || errors.length > 0) return wholeBecause(format === "json5" ? "it uses JSON5-only syntax (such as unquoted keys, single quotes, hex numbers, Infinity or NaN)" : "it could not be parsed");
12465
12821
  if (root.type !== "object") return wholeBecause("its root is not an object");
12466
12822
  const uneditable = uneditableKeyOf(root);
12467
12823
  if (uneditable !== void 0) {
@@ -12490,10 +12846,22 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
12490
12846
  leadingKeys
12491
12847
  });
12492
12848
  } catch (error) {
12849
+ if (error instanceof Json5EditRefusedError) throw error;
12493
12850
  return wholeBecause(`the editor refused it (${stripControlCharacters(formatError(error))})`);
12494
12851
  }
12495
12852
  }
12496
12853
  /**
12854
+ * Thrown instead of rewriting a `json5` file whole (see
12855
+ * {@link serializeSharedConfig}); nothing is written.
12856
+ */
12857
+ var Json5EditRefusedError = class extends Error {
12858
+ constructor({ filePath, reason }) {
12859
+ const subject = filePath === void 0 ? "a shared JSON5 config file" : quoteForLog(filePath);
12860
+ super(`Refusing to update ${subject}: rulesync cannot edit it in place because ${reason}, and rewriting it whole would drop its comments and could change its values. Convert it to JSONC-compatible syntax (double-quoted keys and strings, decimal numbers; comments and trailing commas are fine), or add the entries by hand.`);
12861
+ this.name = "Json5EditRefusedError";
12862
+ }
12863
+ };
12864
+ /**
12497
12865
  * Report, once per run and file, that a JSONC file is being written whole
12498
12866
  * instead of edited in place. Named by `filePath` when the caller knows it;
12499
12867
  * a caller serializing text it never read from disk has no name to give.
@@ -12556,6 +12924,7 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
12556
12924
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
12557
12925
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
12558
12926
  const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
12927
+ const OPENCLAW_CONFIG_SHARED_FILE_KEY = ".openclaw/openclaw.json";
12559
12928
  const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
12560
12929
  const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
12561
12930
  /**
@@ -13011,7 +13380,12 @@ const SHARED_CONFIG_OWNERSHIP = {
13011
13380
  format: "json",
13012
13381
  features: { permissions: {
13013
13382
  kind: "replace-owned-keys",
13014
- ownedKeys: ["permissions"]
13383
+ ownedKeys: [
13384
+ "permissions",
13385
+ "requiredPlugins",
13386
+ "optionalPlugins",
13387
+ "forbiddenPlugins"
13388
+ ]
13015
13389
  } }
13016
13390
  },
13017
13391
  ".config/devin/config.json": {
@@ -13159,10 +13533,27 @@ const SHARED_CONFIG_OWNERSHIP = {
13159
13533
  },
13160
13534
  permissions: {
13161
13535
  kind: "replace-owned-keys",
13162
- ownedKeys: ["permissions", "outputStyle"]
13536
+ ownedKeys: [
13537
+ "permissions",
13538
+ "outputStyle",
13539
+ "general",
13540
+ "tools",
13541
+ "mcp",
13542
+ "agents",
13543
+ "autoMode",
13544
+ "agentsMdExcludes"
13545
+ ]
13163
13546
  }
13164
13547
  }
13165
13548
  },
13549
+ ".trae/permission/global.json": {
13550
+ format: "json",
13551
+ invalidRootPolicy: "error",
13552
+ features: { permissions: {
13553
+ kind: "replace-owned-keys",
13554
+ ownedKeys: ["customProfiles", "resourceAuthorization"]
13555
+ } }
13556
+ },
13166
13557
  ".tabnine/agent/settings.json": {
13167
13558
  format: "json",
13168
13559
  invalidRootPolicy: "error",
@@ -13436,6 +13827,14 @@ const SHARED_CONFIG_OWNERSHIP = {
13436
13827
  ownedKeys: ["toolPermissions"]
13437
13828
  }
13438
13829
  }
13830
+ },
13831
+ [OPENCLAW_CONFIG_SHARED_FILE_KEY]: {
13832
+ format: "json5",
13833
+ invalidRootPolicy: "error",
13834
+ features: { mcp: {
13835
+ kind: "replace-owned-keys",
13836
+ ownedKeys: ["mcp"]
13837
+ } }
13439
13838
  }
13440
13839
  };
13441
13840
  /**
@@ -21978,6 +22377,190 @@ var KiroIdeCommand = class extends KiroCommand {
21978
22377
  }
21979
22378
  };
21980
22379
  //#endregion
22380
+ //#region src/constants/lettacode-paths.ts
22381
+ const LETTACODE_DIR = ".letta";
22382
+ const LETTACODE_SETTINGS_FILE_NAME = "settings.json";
22383
+ const LETTACODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
22384
+ const LETTACODE_IGNORE_FILE_NAME = ".lettaignore";
22385
+ const LETTACODE_AGENTS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "agents");
22386
+ const LETTACODE_PROJECT_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
22387
+ const LETTACODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "skills");
22388
+ const LETTACODE_PROJECT_COMMANDS_DIR_PATH = ".commands";
22389
+ const LETTACODE_GLOBAL_COMMANDS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "commands");
22390
+ //#endregion
22391
+ //#region src/features/commands/lettacode-command.ts
22392
+ /**
22393
+ * The only frontmatter keys Letta Code reads from a custom command
22394
+ * (`parseCommandFile` in `src/cli/commands/custom.ts`). Anything else — e.g.
22395
+ * `allowed-tools`, `model` or `disable-model-invocation` copied over from a
22396
+ * Claude Code command — is parsed and ignored upstream.
22397
+ */
22398
+ const LETTACODE_COMMAND_FRONTMATTER_KEYS = ["description", "argument-hint"];
22399
+ const LettacodeCommandFrontmatterSchema = zod_mini.z.looseObject({
22400
+ description: zod_mini.z.optional(zod_mini.z.string()),
22401
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string())
22402
+ });
22403
+ function pickLettacodeFrontmatter({ section, relativeFilePath, logger }) {
22404
+ if (!isRecord$1(section)) return {};
22405
+ const picked = {};
22406
+ for (const [key, value] of Object.entries(section)) {
22407
+ if (LETTACODE_COMMAND_FRONTMATTER_KEYS.includes(key)) {
22408
+ picked[key] = value;
22409
+ continue;
22410
+ }
22411
+ warnWithFallback(logger, `Letta Code commands: dropping lettacode.${stripControlCharacters(key)} from ${quoteForLog(relativeFilePath)}: Letta Code only reads ${LETTACODE_COMMAND_FRONTMATTER_KEYS.join(" and ")}.`);
22412
+ }
22413
+ return picked;
22414
+ }
22415
+ /**
22416
+ * Letta Code custom slash commands: Markdown prompt files under `.commands/`
22417
+ * (project, at the working-directory root) and `~/.letta/commands/` (global),
22418
+ * discovered recursively. The file basename is the command id
22419
+ * (`git/commit.md` is invoked as `/commit`; `findCustomCommand` matches the
22420
+ * id only), so two files with the same basename collide and the first one
22421
+ * found wins; a project command wins over a global one with the same id. Frontmatter supports `description` (defaulting to the first body
22422
+ * line) and `argument-hint`; the body supports `$ARGUMENTS`, `$1`..`$9`,
22423
+ * `` !`command` `` and `@file`.
22424
+ *
22425
+ * `description` comes from the rulesync frontmatter; a `lettacode:` section
22426
+ * may set `argument-hint` (and override `description`). Other keys in that
22427
+ * section are dropped with a warning because Letta Code never reads them.
22428
+ *
22429
+ * @see https://docs.letta.com/platform/cli/slash-commands
22430
+ * @see https://github.com/letta-ai/letta-code/blob/main/src/cli/commands/custom.ts
22431
+ */
22432
+ var LettacodeCommand = class LettacodeCommand extends ToolCommand {
22433
+ frontmatter;
22434
+ body;
22435
+ constructor({ frontmatter, body, ...rest }) {
22436
+ if (rest.validate) {
22437
+ const result = LettacodeCommandFrontmatterSchema.safeParse(frontmatter);
22438
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
22439
+ }
22440
+ super({
22441
+ ...rest,
22442
+ fileContent: stringifyFrontmatter(body, frontmatter)
22443
+ });
22444
+ this.frontmatter = frontmatter;
22445
+ this.body = body;
22446
+ }
22447
+ static getSettablePaths({ global = false } = {}) {
22448
+ return { relativeDirPath: global ? LETTACODE_GLOBAL_COMMANDS_DIR_PATH : LETTACODE_PROJECT_COMMANDS_DIR_PATH };
22449
+ }
22450
+ getBody() {
22451
+ return this.body;
22452
+ }
22453
+ getFrontmatter() {
22454
+ return this.frontmatter;
22455
+ }
22456
+ toRulesyncCommand() {
22457
+ const { description, ...restFields } = this.frontmatter;
22458
+ const rulesyncFrontmatter = {
22459
+ targets: ["*"],
22460
+ description,
22461
+ ...Object.keys(restFields).length > 0 && { lettacode: restFields }
22462
+ };
22463
+ return new RulesyncCommand({
22464
+ outputRoot: ".",
22465
+ frontmatter: rulesyncFrontmatter,
22466
+ body: this.body,
22467
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
22468
+ relativeFilePath: this.relativeFilePath,
22469
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
22470
+ validate: true
22471
+ });
22472
+ }
22473
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
22474
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
22475
+ const lettacodeFields = pickLettacodeFrontmatter({
22476
+ section: rulesyncFrontmatter.lettacode,
22477
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
22478
+ logger
22479
+ });
22480
+ const frontmatter = {
22481
+ ...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
22482
+ ...lettacodeFields
22483
+ };
22484
+ const paths = this.getSettablePaths({ global });
22485
+ return new LettacodeCommand({
22486
+ outputRoot,
22487
+ frontmatter,
22488
+ body: rulesyncCommand.getBody(),
22489
+ relativeDirPath: paths.relativeDirPath,
22490
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
22491
+ validate,
22492
+ global
22493
+ });
22494
+ }
22495
+ validate() {
22496
+ if (!this.frontmatter) return {
22497
+ success: true,
22498
+ error: null
22499
+ };
22500
+ const result = LettacodeCommandFrontmatterSchema.safeParse(this.frontmatter);
22501
+ if (result.success) return {
22502
+ success: true,
22503
+ error: null
22504
+ };
22505
+ return {
22506
+ success: false,
22507
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
22508
+ };
22509
+ }
22510
+ /**
22511
+ * Warn when two targeted commands share a basename. Letta Code invokes a
22512
+ * command by its basename only, so `git/commit.md` and `release/commit.md`
22513
+ * are both `/commit` and only the first one Letta Code finds is reachable.
22514
+ * Both files are still written, so this warns instead of failing the run.
22515
+ */
22516
+ static validateRulesyncCommands({ rulesyncCommands, logger }) {
22517
+ const pathsById = /* @__PURE__ */ new Map();
22518
+ for (const command of rulesyncCommands) {
22519
+ if (!this.isTargetedByRulesyncCommand(command)) continue;
22520
+ const relativeFilePath = command.getRelativeFilePath();
22521
+ const id = (0, node_path.basename)(relativeFilePath, ".md");
22522
+ pathsById.set(id, [...pathsById.get(id) ?? [], relativeFilePath]);
22523
+ }
22524
+ for (const [id, paths] of pathsById) {
22525
+ if (paths.length < 2) continue;
22526
+ logger.warn(`Letta Code invokes commands by file basename, so ${paths.map((path) => quoteForLog(path)).join(", ")} are all /${stripControlCharacters(id)} and only one of them is reachable. Rename them to make each invocable.`);
22527
+ }
22528
+ }
22529
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
22530
+ return this.isTargetedByRulesyncCommandDefault({
22531
+ rulesyncCommand,
22532
+ toolTarget: "lettacode"
22533
+ });
22534
+ }
22535
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
22536
+ const paths = this.getSettablePaths({ global });
22537
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
22538
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
22539
+ const result = LettacodeCommandFrontmatterSchema.safeParse(frontmatter);
22540
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
22541
+ return new LettacodeCommand({
22542
+ outputRoot,
22543
+ relativeDirPath: paths.relativeDirPath,
22544
+ relativeFilePath,
22545
+ frontmatter: result.data,
22546
+ body: body.trim(),
22547
+ validate,
22548
+ global
22549
+ });
22550
+ }
22551
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
22552
+ return new LettacodeCommand({
22553
+ outputRoot,
22554
+ relativeDirPath,
22555
+ relativeFilePath,
22556
+ frontmatter: {},
22557
+ body: "",
22558
+ validate: false,
22559
+ global
22560
+ });
22561
+ }
22562
+ };
22563
+ //#endregion
21981
22564
  //#region src/constants/mimocode-paths.ts
21982
22565
  /**
21983
22566
  * MiMo Code (Xiaomi's OpenCode fork) reads `.mimocode/` in the project and
@@ -21995,7 +22578,8 @@ const MIMOCODE_LAYOUT = {
21995
22578
  configDir: MIMOCODE_DIR,
21996
22579
  jsonFileName: "mimocode.json",
21997
22580
  jsoncFileName: "mimocode.jsonc",
21998
- readsV2ConfigSpellings: false
22581
+ readsV2ConfigSpellings: false,
22582
+ readsDotDirConfig: false
21999
22583
  };
22000
22584
  //#endregion
22001
22585
  //#region src/constants/opencode-paths.ts
@@ -22022,7 +22606,8 @@ const OPENCODE_LAYOUT = {
22022
22606
  configDir: ".",
22023
22607
  jsonFileName: OPENCODE_JSON_FILE_NAME,
22024
22608
  jsoncFileName: OPENCODE_JSONC_FILE_NAME,
22025
- readsV2ConfigSpellings: true
22609
+ readsV2ConfigSpellings: true,
22610
+ readsDotDirConfig: true
22026
22611
  };
22027
22612
  //#endregion
22028
22613
  //#region src/features/opencode-config.ts
@@ -22056,6 +22641,107 @@ async function readOpencodeConfig({ outputRoot, global = false, layout = OPENCOD
22056
22641
  });
22057
22642
  }
22058
22643
  /**
22644
+ * Reads the project-scope config files OpenCode merges over the root
22645
+ * `opencode.json(c)`: `.opencode/opencode.json`, then `.opencode/opencode.jsonc`,
22646
+ * in the order OpenCode loads them (a later file wins). Returns an empty list
22647
+ * in global mode and for layouts without this location (MiMo Code).
22648
+ *
22649
+ * @see https://opencode.ai/docs/config/#precedence-order
22650
+ * @see https://github.com/anomalyco/opencode/blob/v1.19.3/packages/opencode/src/config/config.ts
22651
+ */
22652
+ async function readOpencodeDotDirConfigLayers({ outputRoot, global = false, layout = OPENCODE_LAYOUT }) {
22653
+ if (global || !layout.readsDotDirConfig) return [];
22654
+ const configDir = (0, node_path.join)(outputRoot, layout.dir);
22655
+ const layers = [];
22656
+ for (const fileName of [layout.jsonFileName, layout.jsoncFileName]) {
22657
+ const fileContent = await readFileContentOrNull((0, node_path.join)(configDir, fileName));
22658
+ if (fileContent === null) continue;
22659
+ layers.push({
22660
+ relativeFilePath: (0, node_path.join)(layout.dir, fileName),
22661
+ configDir,
22662
+ config: parseSharedConfig({
22663
+ format: "jsonc",
22664
+ fileContent
22665
+ })
22666
+ });
22667
+ }
22668
+ return layers;
22669
+ }
22670
+ /**
22671
+ * Reads every OpenCode config file that applies at this scope, lowest
22672
+ * precedence first: the root config (see {@link readOpencodeConfig}), then the
22673
+ * `.opencode/` files (see {@link readOpencodeDotDirConfigLayers}).
22674
+ */
22675
+ async function readOpencodeConfigLayers({ outputRoot, global = false, layout = OPENCODE_LAYOUT }) {
22676
+ return [{
22677
+ configDir: getOpencodeConfigDir({
22678
+ outputRoot,
22679
+ global,
22680
+ layout
22681
+ }),
22682
+ config: await readOpencodeConfig({
22683
+ outputRoot,
22684
+ global,
22685
+ layout
22686
+ })
22687
+ }, ...await readOpencodeDotDirConfigLayers({
22688
+ outputRoot,
22689
+ global,
22690
+ layout
22691
+ })];
22692
+ }
22693
+ /**
22694
+ * Deep-merges `source` over `target` the way OpenCode merges its config files
22695
+ * (remeda's `mergeDeep`): plain objects merge key by key, and any other value —
22696
+ * arrays included — replaces the target's.
22697
+ */
22698
+ function mergeOpencodeConfigValues(target, source) {
22699
+ if (!isRecord$1(target) || !isRecord$1(source)) return source;
22700
+ const merged = {};
22701
+ for (const [key, value] of Object.entries(target)) setOwnEntry(merged, key, value);
22702
+ for (const [key, value] of Object.entries(source)) setOwnEntry(merged, key, Object.hasOwn(target, key) ? mergeOpencodeConfigValues(target[key], value) : value);
22703
+ return merged;
22704
+ }
22705
+ /**
22706
+ * Merges per-file entry maps (the `command` or `agent` entries of each config
22707
+ * layer, lowest precedence first) into one, as OpenCode does. Returns `null`
22708
+ * when no layer has entries.
22709
+ */
22710
+ function mergeOpencodeEntryLayers(layers) {
22711
+ let merged = null;
22712
+ for (const entries of layers) {
22713
+ if (entries === null) continue;
22714
+ const next = mergeOpencodeConfigValues(merged ?? {}, entries);
22715
+ merged = isRecord$1(next) ? next : merged;
22716
+ }
22717
+ return merged;
22718
+ }
22719
+ /**
22720
+ * Warns when a `.opencode/opencode.json(c)` file sets entries that the
22721
+ * generated root config also sets. OpenCode merges those files over the root
22722
+ * config, so their entries win and the generated ones never take effect.
22723
+ *
22724
+ * `generated` maps each top-level key rulesync writes (`mcp`, `permission`) to
22725
+ * its generated value. `overlays` are the `.opencode/` layers, already lowered
22726
+ * by the caller where needed.
22727
+ */
22728
+ function warnOpencodeDotDirOverrides({ generated, overlays, rootFileName, logger }) {
22729
+ if (!logger) return;
22730
+ for (const overlay of overlays) {
22731
+ const overridden = [];
22732
+ for (const [key, value] of Object.entries(generated)) {
22733
+ if (value === void 0 || !Object.hasOwn(overlay.config, key)) continue;
22734
+ const overlayValue = overlay.config[key];
22735
+ if (!isRecord$1(value) || !isRecord$1(overlayValue)) {
22736
+ overridden.push(`"${key}"`);
22737
+ continue;
22738
+ }
22739
+ for (const name of Object.keys(value)) if (Object.hasOwn(overlayValue, name)) overridden.push(`"${key}" > ${quoteValueForWarning(name)}`);
22740
+ }
22741
+ if (overridden.length > 0) logger.warn(`OpenCode merges "${toPosixPath(overlay.relativeFilePath)}" over the generated "${rootFileName}", so its values for these generated entries take precedence: ${overridden.join(", ")}.`);
22742
+ }
22743
+ }
22744
+ /**
22059
22745
  * Narrows an unknown value to a plain record of entries keyed by name, as used
22060
22746
  * for OpenCode's `command` / `agent` config sections. Returns `null` when the
22061
22747
  * value is not a usable object.
@@ -22498,8 +23184,9 @@ var OpenCodeCommand = class extends ToolCommand {
22498
23184
  });
22499
23185
  }
22500
23186
  /**
22501
- * Imports commands defined inline in `opencode.json` / `opencode.jsonc` under
22502
- * the top-level `command` key — or its V2 spelling `commands`, see
23187
+ * Imports commands defined inline in `opencode.json` / `opencode.jsonc` (and,
23188
+ * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
23189
+ * them) under the top-level `command` key — or its V2 spelling `commands`, see
22503
23190
  * `getOpencodeCommandEntries` — (in addition to the Markdown files under
22504
23191
  * `.opencode/commands/`). Each entry's `template` becomes the command body,
22505
23192
  * while `description` / `agent` / `model` / `variant` / `subtask` map to the
@@ -22512,14 +23199,14 @@ var OpenCodeCommand = class extends ToolCommand {
22512
23199
  * @see https://opencode.ai/docs/commands/#json
22513
23200
  */
22514
23201
  static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false } = {}) {
22515
- const commandEntries = getOpencodeCommandEntries({
22516
- config: await readOpencodeConfig({
22517
- outputRoot,
22518
- global,
22519
- layout: this.layout
22520
- }),
23202
+ const commandEntries = mergeOpencodeEntryLayers((await readOpencodeConfigLayers({
23203
+ outputRoot,
23204
+ global,
22521
23205
  layout: this.layout
22522
- });
23206
+ })).map(({ config }) => getOpencodeCommandEntries({
23207
+ config,
23208
+ layout: this.layout
23209
+ })));
22523
23210
  if (!commandEntries) return [];
22524
23211
  const paths = this.getSettablePaths({ global });
22525
23212
  const commands = [];
@@ -23905,6 +24592,7 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23905
24592
  * @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
23906
24593
  * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23907
24594
  * @see https://docs.trae.ai/ide/ignore-files?_lang=en
24595
+ * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
23908
24596
  */
23909
24597
  const TRAE_DIR = ".trae";
23910
24598
  /** Project MCP servers (`mcpServers` object), project scope only. */
@@ -23918,6 +24606,13 @@ const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
23918
24606
  const TRAE_COMMANDS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "commands");
23919
24607
  /** Indexing ignore list (gitignore syntax), `.trae/.ignore`, project scope only. */
23920
24608
  const TRAE_IGNORE_FILE_NAME = ".ignore";
24609
+ /**
24610
+ * Global permission policy: `~/.trae/permission/global.json`, user scope only. It
24611
+ * also holds the custom permission mode (`customProfiles`), so it is edited in
24612
+ * place rather than owned outright.
24613
+ */
24614
+ const TRAE_PERMISSION_DIR_PATH = (0, node_path.join)(TRAE_DIR, "permission");
24615
+ const TRAE_PERMISSION_FILE_NAME = "global.json";
23921
24616
  //#endregion
23922
24617
  //#region src/features/commands/trae-command.ts
23923
24618
  const TraeCommandFrontmatterSchema = zod_mini.z.looseObject({
@@ -24683,6 +25378,17 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24683
25378
  supportsSubdirectory: false
24684
25379
  }
24685
25380
  }],
25381
+ ["lettacode", {
25382
+ class: LettacodeCommand,
25383
+ meta: {
25384
+ extension: "md",
25385
+ supportsProject: true,
25386
+ supportsGlobal: true,
25387
+ isSimulated: false,
25388
+ supportsSubdirectory: true,
25389
+ committedOutput: true
25390
+ }
25391
+ }],
24686
25392
  ["mimocode", {
24687
25393
  class: MimocodeCommand,
24688
25394
  meta: {
@@ -24992,12 +25698,25 @@ var CommandsProcessor = class extends FeatureProcessor {
24992
25698
  return rulesyncCommands;
24993
25699
  }
24994
25700
  /**
25701
+ * Whether {@link loadToolFiles} must not scan the tool's output tree at all.
25702
+ * `skipToolFileScan` targets never scan it; a `committedOutput` target is not
25703
+ * swept at project scope, because nothing on disk tells a hand-written
25704
+ * command in such a committed root apart from one rulesync generated.
25705
+ */
25706
+ skipsToolFileScan({ factory, forDeletion }) {
25707
+ if (factory.meta.skipToolFileScan) return true;
25708
+ return forDeletion && factory.meta.committedOutput === true && !this.global;
25709
+ }
25710
+ /**
24995
25711
  * Implementation of abstract method from FeatureProcessor
24996
25712
  * Load tool-specific command configurations and parse them into ToolCommand instances
24997
25713
  */
24998
25714
  async loadToolFiles({ forDeletion = false } = {}) {
24999
25715
  const factory = this.getFactory(this.toolTarget);
25000
- if (factory.meta.skipToolFileScan) return [];
25716
+ if (this.skipsToolFileScan({
25717
+ factory,
25718
+ forDeletion
25719
+ })) return [];
25001
25720
  const paths = factory.class.getSettablePaths({ global: this.global });
25002
25721
  const outputRootFull = (0, node_path.join)(this.outputRoot, paths.relativeDirPath);
25003
25722
  const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ? `**/*.${factory.meta.extension}` : `*.${factory.meta.extension}`, {
@@ -26714,13 +27433,48 @@ function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, w
26714
27433
  })
26715
27434
  };
26716
27435
  }
27436
+ /**
27437
+ * Why a definition must not be emitted for this tool, or `undefined` when it
27438
+ * can be: it carries a restricting field the tool refuses to honour (see
27439
+ * `skippedWhenFields`).
27440
+ */
27441
+ function describeExportSkipReason({ def, hookType, eventName, converterConfig }) {
27442
+ const field = converterConfig.skippedWhenFields?.find((name) => isNonEmptyString(def[name]));
27443
+ if (field === void 0) return;
27444
+ return `Skipping a "${eventName}" ${hookType} hook (${quoteValueForWarning(def.command ?? def.url)}): this tool does not support its "${field}" condition (${quoteValueForWarning(def[field])}), and without it the hook would run on every call its matcher selects.`;
27445
+ }
27446
+ /**
27447
+ * Whether a definition can be emitted for this tool: its hook type is one the
27448
+ * tool supports, and it carries no restricting field the tool refuses to
27449
+ * honour (warned about here).
27450
+ */
27451
+ function isEmittableDefinition({ def, hookType, eventName, converterConfig, warn }) {
27452
+ if (!isSupportedHookType({
27453
+ type: hookType,
27454
+ converterConfig
27455
+ })) return false;
27456
+ const skipReason = describeExportSkipReason({
27457
+ def,
27458
+ hookType,
27459
+ eventName,
27460
+ converterConfig
27461
+ });
27462
+ if (skipReason !== void 0) {
27463
+ warn?.(skipReason);
27464
+ return false;
27465
+ }
27466
+ return true;
27467
+ }
26717
27468
  function buildToolHooks({ defs, eventName, converterConfig, warn }) {
26718
27469
  const hooks = [];
26719
27470
  for (const def of defs) {
26720
27471
  const hookType = def.type ?? "command";
26721
- if (!isSupportedHookType({
26722
- type: hookType,
26723
- converterConfig
27472
+ if (!isEmittableDefinition({
27473
+ def,
27474
+ hookType,
27475
+ eventName,
27476
+ converterConfig,
27477
+ warn
26724
27478
  })) continue;
26725
27479
  const command = applyCommandPrefix({
26726
27480
  def,
@@ -27292,7 +28046,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
27292
28046
  warn
27293
28047
  }));
27294
28048
  }
27295
- if (defs.length > 0) canonical[eventName] = defs;
28049
+ if (defs.length > 0) {
28050
+ const existing = Object.hasOwn(canonical, eventName) ? canonical[eventName] : void 0;
28051
+ canonical[eventName] = existing ? [...existing, ...defs] : defs;
28052
+ }
27296
28053
  }
27297
28054
  return canonical;
27298
28055
  }
@@ -32002,6 +32759,16 @@ const GROKCLI_CONVERTER_CONFIG = {
32002
32759
  tool: "env",
32003
32760
  commandOnly: true
32004
32761
  }],
32762
+ arrayPassthroughFields: [{
32763
+ canonical: "args",
32764
+ tool: "args",
32765
+ commandOnly: true
32766
+ }],
32767
+ skippedWhenFields: ["if"],
32768
+ stringPassthroughFields: [{
32769
+ canonical: "if",
32770
+ tool: "if"
32771
+ }],
32005
32772
  noMatcherEvents: /* @__PURE__ */ new Set(["stop", "beforeSubmitPrompt"])
32006
32773
  };
32007
32774
  /**
@@ -33498,15 +34265,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
33498
34265
  }
33499
34266
  };
33500
34267
  //#endregion
33501
- //#region src/constants/lettacode-paths.ts
33502
- const LETTACODE_DIR = ".letta";
33503
- const LETTACODE_SETTINGS_FILE_NAME = "settings.json";
33504
- const LETTACODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
33505
- const LETTACODE_IGNORE_FILE_NAME = ".lettaignore";
33506
- const LETTACODE_AGENTS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "agents");
33507
- const LETTACODE_PROJECT_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
33508
- const LETTACODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "skills");
33509
- //#endregion
33510
34268
  //#region src/features/hooks/lettacode-hooks.ts
33511
34269
  const LETTACODE_CONVERTER_CONFIG = {
33512
34270
  supportedEvents: LETTACODE_HOOK_EVENTS,
@@ -43195,8 +43953,10 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
43195
43953
  /**
43196
43954
  * Crush-only MCP fields with no canonical counterpart, authored under a
43197
43955
  * `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
43198
- * name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers and
43199
- * `sessionless` marks a server that issues no `Mcp-Session-Id`.
43956
+ * name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers,
43957
+ * `sessionless` marks a server that issues no `Mcp-Session-Id`; and
43958
+ * `channel_enabled` / `channel_reply` (v0.97.0) turn a server that implements
43959
+ * Claude Channels into a channel and route replies back through its tools.
43200
43960
  * `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
43201
43961
  * never reaches another tool's config; `fromRulesyncMcp` re-merges them from
43202
43962
  * the raw source JSON, honouring the raw Crush spelling as a fallback for an
@@ -43235,6 +43995,18 @@ const CRUSH_ONLY_KEYS = [
43235
43995
  crush: "sessionless",
43236
43996
  accepts: (v) => typeof v === "boolean",
43237
43997
  rawFallback: true
43998
+ },
43999
+ {
44000
+ canonical: "crushChannelEnabled",
44001
+ crush: "channel_enabled",
44002
+ accepts: (v) => typeof v === "boolean",
44003
+ rawFallback: true
44004
+ },
44005
+ {
44006
+ canonical: "crushChannelReply",
44007
+ crush: "channel_reply",
44008
+ accepts: (v) => CrushChannelReplySchema.safeParse(v).success,
44009
+ rawFallback: true
43238
44010
  }
43239
44011
  ];
43240
44012
  /**
@@ -43242,12 +44014,16 @@ const CRUSH_ONLY_KEYS = [
43242
44014
  * canonical name, so `convertToCrushFormat` sees them after the shared map
43243
44015
  * stripped them.
43244
44016
  */
43245
- function readCrushOnlyKeys(rawServer) {
44017
+ function readCrushOnlyKeys({ serverName, rawServer, logger }) {
43246
44018
  const result = {};
43247
44019
  if (!isRecord$1(rawServer)) return result;
43248
44020
  for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
43249
44021
  const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
43250
44022
  if (value !== void 0) result[canonical] = value;
44023
+ else if (canonical === "crushChannelReply") {
44024
+ const rejected = [canonical, crush].find((key) => rawServer[key] !== void 0);
44025
+ if (rejected !== void 0) logger?.warn(`Crush MCP server "${serverName}": ignoring "${rejected}" because it does not match Crush's channel_reply shape ({ user?, group?: { tool, target_param?, target_meta? }, message_param?, suppress_tools? }).`);
44026
+ }
43251
44027
  }
43252
44028
  return result;
43253
44029
  }
@@ -43372,8 +44148,8 @@ function convertToCrushFormat(mcpServers, logger) {
43372
44148
  * servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
43373
44149
  * server with a `command` is stdio already); `http` and `sse` are kept as the
43374
44150
  * canonical transport names; `disabled_tools`/`enabled_tools` become the
43375
- * canonical `disabledTools`/`enabledTools`; the Crush-only OAuth and
43376
- * `sessionless` fields are lifted into their `crush*` authoring keys (a value
44151
+ * canonical `disabledTools`/`enabledTools`; the Crush-only OAuth,
44152
+ * `sessionless` and channel fields are lifted into their `crush*` authoring keys (a value
43377
44153
  * of the wrong type is dropped, as Crush itself would refuse it), and any
43378
44154
  * other key is kept verbatim so an import preserves what a hand-authored
43379
44155
  * entry declared.
@@ -43464,7 +44240,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
43464
44240
  });
43465
44241
  const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
43466
44242
  ...serverConfig,
43467
- ...readCrushOnlyKeys(rulesyncMcp.getRawMcpServer(serverName))
44243
+ ...readCrushOnlyKeys({
44244
+ serverName,
44245
+ rawServer: rulesyncMcp.getRawMcpServer(serverName),
44246
+ logger
44247
+ })
43468
44248
  }])), logger);
43469
44249
  return new CrushMcp({
43470
44250
  outputRoot,
@@ -44057,12 +44837,19 @@ var DevinPluginMcp = class DevinPluginMcp extends DevinMcp {
44057
44837
  * only the default location. Project-scoped assets live under a `.dsh/`
44058
44838
  * directory at the project root.
44059
44839
  *
44060
- * Rules: `dsh-agent-instructions` loads the user-global `$DSH_HOME/AGENTS.md`
44061
- * (root file only — the user-global file has no `.local.md` overlay), then the
44062
- * project chain of `AGENTS.md` / `CLAUDE.md` files from the project root (the
44063
- * nearest ancestor holding `.git`) down to the session cwd, broad to specific.
44064
- * Rulesync emits `AGENTS.md` only; `CLAUDE.md` is the `claudecode` target's
44065
- * file, and a `CLAUDE.md` duplicating its sibling `AGENTS.md` renders once.
44840
+ * Rules: `dsh-agent-instructions` loads the user-global chain —
44841
+ * `$DSH_HOME/AGENTS.md`, then `<agentsHome>/AGENTS.md` (`$DSH_AGENTS_HOME`,
44842
+ * default `~/.agents`, since dsh-v0.2.1-alpha.2) — then the project chain of
44843
+ * `AGENTS.md` / `CLAUDE.md` files plus their `AGENTS.local.md` /
44844
+ * `CLAUDE.local.md` overlays from the project root (the nearest ancestor
44845
+ * holding `.git`) down to the session cwd, broad to specific. Neither
44846
+ * user-global root has a `.local.md` overlay. The two user-global roots share
44847
+ * one dedupe group, so the shared-root file collapses behind the harness-home
44848
+ * file only when both match after trimming; otherwise both load. Rulesync
44849
+ * writes only `$DSH_HOME/AGENTS.md` globally (never the shared
44850
+ * `~/.agents/AGENTS.md`), and `AGENTS.md` / `AGENTS.local.md` in the project;
44851
+ * `CLAUDE.md` is the `claudecode` target's file, and a `CLAUDE.md` duplicating
44852
+ * its sibling `AGENTS.md` renders once.
44066
44853
  *
44067
44854
  * Skills: `dsh-skill-filesystem` scans `<projectRoot>/.dsh/skills` (rank 100),
44068
44855
  * `<projectRoot>/.agents/skills` (200), `<dshHome>/skills` (400) and
@@ -46406,6 +47193,20 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
46406
47193
  if (transport === "http" || transport === "streamable-http") return "http";
46407
47194
  return hasCommand ? "stdio" : "http";
46408
47195
  }
47196
+ /**
47197
+ * The only `executor` value Kimi Code accepts on a stdio server. 2.2.0 removed
47198
+ * the never-implemented `kaos` option and narrowed the schema to
47199
+ * `z.enum(['local'])`, so any other value makes Kimi fail validation when it
47200
+ * loads `mcp.json`.
47201
+ * @see https://github.com/MoonshotAI/kimi-code/pull/4184
47202
+ */
47203
+ const KIMI_CODE_MCP_EXECUTOR = "local";
47204
+ /** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
47205
+ function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
47206
+ if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
47207
+ warnWithFallback(logger, `Kimi Code MCP: dropping executor ${JSON.stringify(server.executor)} from "${name}" because Kimi Code only accepts "${KIMI_CODE_MCP_EXECUTOR}".`);
47208
+ delete server.executor;
47209
+ }
46409
47210
  function toKimiCodeServer({ name, server, logger }) {
46410
47211
  const transport = server.transport ?? server.type;
46411
47212
  if (transport === "ws") {
@@ -46444,6 +47245,11 @@ function toKimiCodeServer({ name, server, logger }) {
46444
47245
  "enabledTools",
46445
47246
  "disabledTools"
46446
47247
  ]) if (server[field] !== void 0) converted[field] = server[field];
47248
+ dropUnsupportedKimiCodeExecutor({
47249
+ name,
47250
+ server: converted,
47251
+ logger
47252
+ });
46447
47253
  if (server.disabled === true) converted.enabled = false;
46448
47254
  return converted;
46449
47255
  }
@@ -46459,9 +47265,14 @@ function toKimiCodeServers({ servers, logger }) {
46459
47265
  }
46460
47266
  return result;
46461
47267
  }
46462
- function fromKimiCodeServers(servers) {
47268
+ function fromKimiCodeServers({ servers, logger }) {
46463
47269
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46464
47270
  const { transport, enabled, ...rest } = server;
47271
+ dropUnsupportedKimiCodeExecutor({
47272
+ name,
47273
+ server: rest,
47274
+ logger
47275
+ });
46465
47276
  const type = transport === "stdio" || transport === "sse" || transport === "http" ? transport : void 0;
46466
47277
  return [name, {
46467
47278
  ...rest,
@@ -46576,9 +47387,11 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46576
47387
  * a different file that only the async `fromFile` can read.
46577
47388
  */
46578
47389
  configDefaults;
46579
- constructor(params) {
47390
+ logger;
47391
+ constructor({ configDefaults, logger, ...params }) {
46580
47392
  super(params);
46581
- this.configDefaults = params.configDefaults ?? {};
47393
+ this.configDefaults = configDefaults ?? {};
47394
+ this.logger = logger;
46582
47395
  try {
46583
47396
  this.json = this.fileContent ? JSON.parse(this.fileContent) : {};
46584
47397
  } catch (error) {
@@ -46594,7 +47407,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46594
47407
  relativeFilePath: KIMI_CODE_MCP_FILE_NAME
46595
47408
  };
46596
47409
  }
46597
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
47410
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
46598
47411
  const paths = this.getSettablePaths({ global });
46599
47412
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
46600
47413
  return new KimiCodeMcp({
@@ -46604,7 +47417,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46604
47417
  fileContent,
46605
47418
  validate,
46606
47419
  global,
46607
- configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {}
47420
+ configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {},
47421
+ logger
46608
47422
  });
46609
47423
  }
46610
47424
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
@@ -46678,7 +47492,10 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46678
47492
  relativeFilePath: RULESYNC_MCP_FILE_NAME,
46679
47493
  fileContent: JSON.stringify({
46680
47494
  ...this.json,
46681
- mcpServers: fromKimiCodeServers(isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {}),
47495
+ mcpServers: fromKimiCodeServers({
47496
+ servers: isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {},
47497
+ logger: this.logger
47498
+ }),
46682
47499
  ...Object.keys(this.configDefaults).length > 0 && { "kimi-code": this.configDefaults }
46683
47500
  }, null, 2)
46684
47501
  });
@@ -47119,7 +47936,13 @@ var OpencodeMcp = class extends ToolMcp {
47119
47936
  fileContent = await readFileContentOrNull(jsonPath);
47120
47937
  if (fileContent) relativeFilePath = this.layout.jsonFileName;
47121
47938
  }
47122
- const json = (0, jsonc_parser.parse)(fileContent ?? "{\"mcp\":{}}");
47939
+ const fileContentToUse = fileContent ?? "{\"mcp\":{}}";
47940
+ const overlays = await readOpencodeDotDirConfigLayers({
47941
+ outputRoot,
47942
+ global,
47943
+ layout: this.layout
47944
+ });
47945
+ const json = overlays.length === 0 ? (0, jsonc_parser.parse)(fileContentToUse) : [(0, jsonc_parser.parse)(fileContentToUse), ...overlays.map(({ config }) => config)].map((layer) => this.lowerConfig(layer, logger)).reduce(mergeOpencodeConfigValues);
47123
47946
  const newJson = {
47124
47947
  ...json,
47125
47948
  mcp: json.mcp ?? {}
@@ -47156,6 +47979,27 @@ var OpencodeMcp = class extends ToolMcp {
47156
47979
  if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
47157
47980
  }
47158
47981
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
47982
+ const generatedMcp = undefinedWhenEmpty(convertedMcp);
47983
+ const generatedTools = Object.keys(mcpTools).length > 0 ? mcpTools : void 0;
47984
+ warnOpencodeDotDirOverrides({
47985
+ generated: {
47986
+ mcp: generatedMcp,
47987
+ tools: generatedTools
47988
+ },
47989
+ overlays: (await readOpencodeDotDirConfigLayers({
47990
+ outputRoot,
47991
+ global,
47992
+ layout: this.layout
47993
+ })).map((overlay) => {
47994
+ const lowered = this.lowerConfig(overlay.config);
47995
+ return {
47996
+ ...overlay,
47997
+ config: isRecord$1(lowered) ? lowered : {}
47998
+ };
47999
+ }),
48000
+ rootFileName: relativeFilePath,
48001
+ logger
48002
+ });
47159
48003
  return new this({
47160
48004
  outputRoot,
47161
48005
  relativeDirPath: basePaths.relativeDirPath,
@@ -47165,8 +48009,8 @@ var OpencodeMcp = class extends ToolMcp {
47165
48009
  feature: "mcp",
47166
48010
  existingContent: fileContent ?? "",
47167
48011
  patch: {
47168
- mcp: undefinedWhenEmpty(convertedMcp),
47169
- tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
48012
+ mcp: generatedMcp,
48013
+ tools: generatedTools
47170
48014
  },
47171
48015
  filePath: (0, node_path.join)(jsonDir, relativeFilePath),
47172
48016
  logger
@@ -47812,6 +48656,358 @@ var OmpMcp = class OmpMcp extends ToolMcp {
47812
48656
  }
47813
48657
  };
47814
48658
  //#endregion
48659
+ //#region src/constants/openclaw-paths.ts
48660
+ const OPENCLAW_WORKSPACE_DIR_PATH = (0, node_path.join)(".openclaw", "workspace");
48661
+ const OPENCLAW_RULE_FILE_NAME = "AGENTS.md";
48662
+ const OPENCLAW_CONFIG_DIR_PATH = ".openclaw";
48663
+ const OPENCLAW_CONFIG_FILE_NAME = "openclaw.json";
48664
+ const OPENCLAW_MCP_SERVERS_KEY = "servers";
48665
+ //#endregion
48666
+ //#region src/features/mcp/openclaw-mcp.ts
48667
+ const OPENCLAW_GLOBAL_ONLY_MESSAGE = "OpenClaw MCP is global-only; use --global to sync ~/.openclaw/openclaw.json";
48668
+ /**
48669
+ * Single spelling of the openclaw.json codec/policy, matching its
48670
+ * `SHARED_CONFIG_OWNERSHIP` declaration: the file is JSON5, and a file whose
48671
+ * root is not an object is refused rather than replaced, since it also holds
48672
+ * every other Gateway setting.
48673
+ */
48674
+ function parseOpenclawConfig(fileContent, filePath) {
48675
+ return parseSharedConfig({
48676
+ format: "json5",
48677
+ fileContent,
48678
+ filePath,
48679
+ invalidRootPolicy: "error"
48680
+ });
48681
+ }
48682
+ const OPENCLAW_OAUTH_STRING_KEYS = [
48683
+ "authProfileId",
48684
+ "scope",
48685
+ "redirectUrl",
48686
+ "clientMetadataUrl"
48687
+ ];
48688
+ const OPENCLAW_OAUTH_IDENTITIES = /* @__PURE__ */ new Set(["shared", "per-requester"]);
48689
+ /**
48690
+ * Copies the `oauth` keys OpenClaw accepts. A known key with a value its
48691
+ * schema rejects (an `identity` other than `shared` / `per-requester`, a blank
48692
+ * string) is dropped with a warning, since the Gateway would refuse to start
48693
+ * on it; keys OpenClaw does not know are left behind silently, as they belong
48694
+ * to other tools' `oauth` blocks.
48695
+ */
48696
+ function copyOpenclawOauth({ source, serverName, logger }) {
48697
+ const oauth = {};
48698
+ const dropped = [];
48699
+ if (source.identity !== void 0) {
48700
+ if (typeof source.identity === "string" && OPENCLAW_OAUTH_IDENTITIES.has(source.identity)) oauth.identity = source.identity;
48701
+ else dropped.push("identity");
48702
+ }
48703
+ for (const key of OPENCLAW_OAUTH_STRING_KEYS) {
48704
+ const value = source[key];
48705
+ if (value === void 0) continue;
48706
+ if (typeof value === "string" && value.trim() !== "") oauth[key] = value;
48707
+ else dropped.push(key);
48708
+ }
48709
+ if (dropped.length > 0 && serverName !== void 0) logger?.warn(`OpenClaw MCP: dropping invalid oauth ${dropped.map((key) => `"${key}"`).join(", ")} of ${quoteValueForWarning(serverName)}; OpenClaw's config validation would reject it.`);
48710
+ return Object.keys(oauth).length > 0 ? oauth : void 0;
48711
+ }
48712
+ const OPENCLAW_CODEX_APPROVAL_MODES = /* @__PURE__ */ new Set([
48713
+ "auto",
48714
+ "prompt",
48715
+ "approve"
48716
+ ]);
48717
+ /**
48718
+ * Copies the OpenClaw-only per-server fields that have no canonical
48719
+ * counterpart — `auth`, `oauth`, the TLS controls, the timeouts in
48720
+ * milliseconds, `supportsParallelToolCalls` and the `codex` projection — from
48721
+ * `source` to `target`, in both directions. OpenClaw validates its config
48722
+ * strictly (`oauth`, `toolFilter` and `codex` are strict objects) and refuses
48723
+ * to start the Gateway on a value it rejects, so only the documented keys with
48724
+ * the documented value types are copied; a same-named key another tool reads
48725
+ * differently (Hermes' snake_case `oauth` keys, say) is left behind.
48726
+ *
48727
+ * @see https://github.com/openclaw/openclaw/blob/v2026.10.4/src/config/zod-schema.mcp-server.ts
48728
+ */
48729
+ function copyOpenclawOnlyFields(source, target, { serverName, logger } = {}) {
48730
+ let copied = false;
48731
+ if (source.auth === "oauth") {
48732
+ target.auth = "oauth";
48733
+ copied = true;
48734
+ }
48735
+ if (isRecord$1(source.oauth)) {
48736
+ const oauth = copyOpenclawOauth({
48737
+ source: source.oauth,
48738
+ serverName,
48739
+ logger
48740
+ });
48741
+ if (oauth) {
48742
+ target.oauth = oauth;
48743
+ copied = true;
48744
+ }
48745
+ }
48746
+ for (const key of ["clientCert", "clientKey"]) if (typeof source[key] === "string") {
48747
+ target[key] = source[key];
48748
+ copied = true;
48749
+ }
48750
+ for (const key of ["sslVerify", "supportsParallelToolCalls"]) if (typeof source[key] === "boolean") {
48751
+ target[key] = source[key];
48752
+ copied = true;
48753
+ }
48754
+ for (const key of ["requestTimeoutMs", "connectionTimeoutMs"]) {
48755
+ const value = source[key];
48756
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
48757
+ target[key] = value;
48758
+ copied = true;
48759
+ }
48760
+ }
48761
+ if (isRecord$1(source.codex)) {
48762
+ const codex = {};
48763
+ if (isStringArray$1(source.codex.agents) && source.codex.agents.length > 0) codex.agents = [...source.codex.agents];
48764
+ const mode = source.codex.defaultToolsApprovalMode;
48765
+ if (typeof mode === "string" && OPENCLAW_CODEX_APPROVAL_MODES.has(mode)) codex.defaultToolsApprovalMode = mode;
48766
+ if (Object.keys(codex).length > 0) {
48767
+ target.codex = codex;
48768
+ copied = true;
48769
+ }
48770
+ }
48771
+ return copied;
48772
+ }
48773
+ /** A string map without pollution keys, or `undefined` when nothing is left. */
48774
+ function copyStringMap(value) {
48775
+ if (!isPlainObject$1(value)) return void 0;
48776
+ const result = {};
48777
+ for (const [key, entry] of Object.entries(value)) {
48778
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
48779
+ if (typeof entry === "string") result[key] = entry;
48780
+ else if (typeof entry === "number" || typeof entry === "boolean") result[key] = String(entry);
48781
+ }
48782
+ return Object.keys(result).length > 0 ? result : void 0;
48783
+ }
48784
+ /** A non-empty `toolFilter` list, trimmed of blank entries OpenClaw rejects. */
48785
+ function toolFilterList(value) {
48786
+ if (!isStringArray$1(value)) return void 0;
48787
+ const list = value.map((entry) => entry.trim()).filter((entry) => entry !== "");
48788
+ return list.length > 0 ? list : void 0;
48789
+ }
48790
+ function convertServerToOpenclaw({ name, config, logger }) {
48791
+ if (declaresNoTransport(config)) return warnAndSkipMcpServer({
48792
+ toolName: "OpenClaw",
48793
+ serverName: name,
48794
+ reason: "no transport",
48795
+ logger
48796
+ });
48797
+ const out = {};
48798
+ if (isRemoteMcpServer(config)) {
48799
+ const url = resolveRemoteMcpUrl(config);
48800
+ if (!url) return warnAndSkipMcpServer({
48801
+ toolName: "OpenClaw",
48802
+ serverName: name,
48803
+ reason: "a remote transport without a url",
48804
+ logger
48805
+ });
48806
+ const stated = config.type ?? config.transport;
48807
+ if (stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) return warnAndSkipMcpServer({
48808
+ toolName: "OpenClaw",
48809
+ serverName: name,
48810
+ reason: "a WebSocket transport, which OpenClaw does not offer (only streamable-http and sse)",
48811
+ logger
48812
+ });
48813
+ out.url = url;
48814
+ out.transport = stated === "sse" ? "sse" : "streamable-http";
48815
+ const headers = copyStringMap(config.headers);
48816
+ if (headers) out.headers = headers;
48817
+ } else {
48818
+ const [command, ...args] = resolveLocalMcpCommand(config);
48819
+ if (!command) return warnAndSkipMcpServer({
48820
+ toolName: "OpenClaw",
48821
+ serverName: name,
48822
+ reason: "a stdio transport without a command",
48823
+ logger
48824
+ });
48825
+ out.command = command;
48826
+ if (args.length > 0) out.args = args;
48827
+ const env = copyStringMap(config.env);
48828
+ if (env) out.env = env;
48829
+ if (typeof config.cwd === "string") out.cwd = config.cwd;
48830
+ }
48831
+ if (config.disabled === true) out.enabled = false;
48832
+ const include = toolFilterList(config.enabledTools);
48833
+ const exclude = toolFilterList(config.disabledTools);
48834
+ if (include || exclude) out.toolFilter = {
48835
+ ...include && { include },
48836
+ ...exclude && { exclude }
48837
+ };
48838
+ copyOpenclawOnlyFields(config, out, {
48839
+ serverName: name,
48840
+ logger
48841
+ });
48842
+ return out;
48843
+ }
48844
+ /**
48845
+ * Converts canonical rulesync servers into OpenClaw `mcp.servers` entries.
48846
+ * Only keys OpenClaw accepts are written — never canonical-only ones such as
48847
+ * `type` or `disabled`, which its strict config validation would answer by
48848
+ * refusing to start the Gateway.
48849
+ */
48850
+ function convertToOpenclawFormat({ mcpServers, logger }) {
48851
+ const result = {};
48852
+ for (const [name, config] of Object.entries(mcpServers)) {
48853
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
48854
+ const converted = convertServerToOpenclaw({
48855
+ name,
48856
+ config,
48857
+ logger
48858
+ });
48859
+ if (converted) result[name] = converted;
48860
+ }
48861
+ return result;
48862
+ }
48863
+ /**
48864
+ * Converts OpenClaw `mcp.servers` entries back into canonical rulesync
48865
+ * servers. OpenClaw-only fields go into the `openclaw.mcpServers` override
48866
+ * block (the whole server, since an override entry replaces the shared one for
48867
+ * that tool), so they survive an import/generate round trip without leaking
48868
+ * into every other tool's config.
48869
+ */
48870
+ function convertFromOpenclawFormat(servers) {
48871
+ const mcpServers = {};
48872
+ const openclawOverrides = {};
48873
+ for (const [name, config] of Object.entries(servers)) {
48874
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
48875
+ const server = {};
48876
+ if (typeof config.command === "string") {
48877
+ server.command = config.command;
48878
+ if (isStringArray$1(config.args)) server.args = config.args;
48879
+ const env = copyStringMap(config.env);
48880
+ if (env) server.env = env;
48881
+ if (typeof config.cwd === "string") server.cwd = config.cwd;
48882
+ } else if (typeof config.url === "string") {
48883
+ server.type = config.transport === "sse" ? "sse" : "http";
48884
+ server.url = config.url;
48885
+ const headers = copyStringMap(config.headers);
48886
+ if (headers) server.headers = headers;
48887
+ }
48888
+ if (config.enabled === false) server.disabled = true;
48889
+ if (isRecord$1(config.toolFilter)) {
48890
+ const include = toolFilterList(config.toolFilter.include);
48891
+ const exclude = toolFilterList(config.toolFilter.exclude);
48892
+ if (include) server.enabledTools = include;
48893
+ if (exclude) server.disabledTools = exclude;
48894
+ }
48895
+ mcpServers[name] = server;
48896
+ const openclawServer = { ...server };
48897
+ if (copyOpenclawOnlyFields(config, openclawServer)) openclawOverrides[name] = openclawServer;
48898
+ }
48899
+ return {
48900
+ mcpServers,
48901
+ openclawOverrides
48902
+ };
48903
+ }
48904
+ /**
48905
+ * OpenClaw MCP servers.
48906
+ *
48907
+ * OpenClaw keeps its MCP registry under `mcp.servers` of the Gateway config
48908
+ * `~/.openclaw/openclaw.json` (JSON5; global only — there is no project-scoped
48909
+ * MCP location). That file also holds credentials and every other Gateway
48910
+ * setting, so generation merges rulesync's servers into `mcp.servers` by name,
48911
+ * keeps servers rulesync does not define and every other key, and never deletes
48912
+ * the file.
48913
+ *
48914
+ * @see https://docs.openclaw.ai/gateway/config-extensions
48915
+ * @see https://docs.openclaw.ai/tools/mcp
48916
+ */
48917
+ var OpenclawMcp = class OpenclawMcp extends ToolMcp {
48918
+ config;
48919
+ constructor(params) {
48920
+ super(params);
48921
+ this.config = parseOpenclawConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
48922
+ }
48923
+ getConfig() {
48924
+ return this.config;
48925
+ }
48926
+ isDeletable() {
48927
+ return false;
48928
+ }
48929
+ static getSettablePaths(_options = {}) {
48930
+ return {
48931
+ relativeDirPath: OPENCLAW_CONFIG_DIR_PATH,
48932
+ relativeFilePath: OPENCLAW_CONFIG_FILE_NAME
48933
+ };
48934
+ }
48935
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
48936
+ if (!global) throw new Error(OPENCLAW_GLOBAL_ONLY_MESSAGE);
48937
+ const paths = this.getSettablePaths({ global });
48938
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
48939
+ return new OpenclawMcp({
48940
+ outputRoot,
48941
+ relativeDirPath: paths.relativeDirPath,
48942
+ relativeFilePath: paths.relativeFilePath,
48943
+ fileContent,
48944
+ validate,
48945
+ global
48946
+ });
48947
+ }
48948
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
48949
+ if (!global) throw new Error(OPENCLAW_GLOBAL_ONLY_MESSAGE);
48950
+ const paths = this.getSettablePaths({ global });
48951
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
48952
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
48953
+ const existing = parseOpenclawConfig(existingContent, filePath);
48954
+ const existingMcp = isRecord$1(existing["mcp"]) ? existing["mcp"] : {};
48955
+ const existingServers = isRecord$1(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
48956
+ const converted = convertToOpenclawFormat({
48957
+ mcpServers: rulesyncMcp.getMcpServers(),
48958
+ logger
48959
+ });
48960
+ const servers = {
48961
+ ...existingServers,
48962
+ ...converted
48963
+ };
48964
+ const kept = Object.keys(existingServers).filter((name) => !PROTOTYPE_POLLUTION_KEYS.has(name) && !Object.hasOwn(converted, name));
48965
+ if (kept.length > 0) logger?.warn(`OpenClaw MCP servers not defined in rulesync are kept in ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${kept.map((name) => quoteValueForWarning(name)).join(", ")}. Remove them with 'openclaw mcp unset <name>' if they are no longer wanted.`);
48966
+ return new OpenclawMcp({
48967
+ outputRoot,
48968
+ relativeDirPath: paths.relativeDirPath,
48969
+ relativeFilePath: paths.relativeFilePath,
48970
+ fileContent: applySharedConfigPatch({
48971
+ fileKey: sharedConfigFileKey(paths),
48972
+ feature: "mcp",
48973
+ existingContent,
48974
+ patch: { ["mcp"]: {
48975
+ ...existingMcp,
48976
+ [OPENCLAW_MCP_SERVERS_KEY]: servers
48977
+ } },
48978
+ filePath,
48979
+ logger
48980
+ }),
48981
+ validate,
48982
+ global
48983
+ });
48984
+ }
48985
+ toRulesyncMcp() {
48986
+ const mcp = isRecord$1(this.config["mcp"]) ? this.config["mcp"] : {};
48987
+ const { mcpServers, openclawOverrides } = convertFromOpenclawFormat(isRecord$1(mcp["servers"]) ? mcp[OPENCLAW_MCP_SERVERS_KEY] : {});
48988
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
48989
+ mcpServers,
48990
+ ...Object.keys(openclawOverrides).length > 0 && { openclaw: { mcpServers: openclawOverrides } }
48991
+ }, null, 2) });
48992
+ }
48993
+ validate() {
48994
+ return {
48995
+ success: true,
48996
+ error: null
48997
+ };
48998
+ }
48999
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
49000
+ return new OpenclawMcp({
49001
+ outputRoot,
49002
+ relativeDirPath,
49003
+ relativeFilePath,
49004
+ fileContent: "",
49005
+ validate: false,
49006
+ global
49007
+ });
49008
+ }
49009
+ };
49010
+ //#endregion
47815
49011
  //#region src/features/mcp/pi-mcp.ts
47816
49012
  /**
47817
49013
  * Canonical keys consumed by the conversion below (or with no Pi counterpart),
@@ -50866,6 +52062,128 @@ function asZcodeRemoteType(stated, url) {
50866
52062
  if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
50867
52063
  }
50868
52064
  /**
52065
+ * The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
52066
+ * remote server, and which of them are required. Read off the 3.14.3/3.15.3
52067
+ * runtime config schema, since ZCode's MCP page only says to "enable OAuth for
52068
+ * the service in its configuration". Both variants are strict, so any other
52069
+ * key would make ZCode skip the server.
52070
+ */
52071
+ const ZCODE_OAUTH_FLOWS = {
52072
+ client_credentials: {
52073
+ required: ["clientId", "clientSecret"],
52074
+ optional: ["clientName", "scope"]
52075
+ },
52076
+ authorization_code: {
52077
+ required: [],
52078
+ optional: [
52079
+ "clientId",
52080
+ "clientSecret",
52081
+ "clientName",
52082
+ "redirectPath",
52083
+ "scope"
52084
+ ]
52085
+ }
52086
+ };
52087
+ /**
52088
+ * The `protocolVersion` values ZCode's server schema accepts (an enum on a
52089
+ * strict object, so any other value makes ZCode skip the server).
52090
+ */
52091
+ const ZCODE_PROTOCOL_VERSIONS = /* @__PURE__ */ new Set([
52092
+ "auto",
52093
+ "legacy",
52094
+ "2026-07-28"
52095
+ ]);
52096
+ /**
52097
+ * The namespaced canonical key that carries ZCode's `oauth`. The canonical
52098
+ * `oauth` already holds Claude Code's `{ clientId }` object, and ZCode's shape
52099
+ * may carry a client secret, so it is kept apart (the `crushOauth*`
52100
+ * precedent) and stripped by `RulesyncMcp.getMcpServers()`.
52101
+ */
52102
+ const ZCODE_OAUTH_CANONICAL_KEY = "zcodeOauth";
52103
+ /**
52104
+ * Whether an `oauth` value names one of ZCode's flows.
52105
+ */
52106
+ function isZcodeOauth(value) {
52107
+ return isRecord$1(value) && typeof value.type === "string" && value.type in ZCODE_OAUTH_FLOWS;
52108
+ }
52109
+ /**
52110
+ * Build ZCode's `oauth` from a `zcodeOauth` value: only the keys of the named
52111
+ * flow are kept, as strings. A value naming no ZCode flow, or missing a key
52112
+ * the flow requires, is dropped with a warning instead of making ZCode skip
52113
+ * the server.
52114
+ */
52115
+ function toZcodeOauth({ serverName, value, logger }) {
52116
+ if (value === void 0) return void 0;
52117
+ if (!isZcodeOauth(value)) {
52118
+ logger?.warn(`ZCode MCP server "${serverName}": dropping zcodeOauth, as its type must be "client_credentials" or "authorization_code".`);
52119
+ return;
52120
+ }
52121
+ const type = String(value.type);
52122
+ const flow = ZCODE_OAUTH_FLOWS[type];
52123
+ if (!flow) return void 0;
52124
+ const missing = flow.required.filter((key) => typeof value[key] !== "string");
52125
+ if (missing.length > 0) {
52126
+ logger?.warn(`ZCode MCP server "${serverName}": dropping zcodeOauth, as the ${type} flow requires ${missing.join(", ")}.`);
52127
+ return;
52128
+ }
52129
+ const allowed = /* @__PURE__ */ new Set([...flow.required, ...flow.optional]);
52130
+ const oauth = { type };
52131
+ const ignored = [];
52132
+ for (const [key, entry] of Object.entries(value)) {
52133
+ if (key === "type" || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
52134
+ if (allowed.has(key) && typeof entry === "string") oauth[key] = entry;
52135
+ else ignored.push(key);
52136
+ }
52137
+ if (ignored.length > 0) logger?.warn(`ZCode MCP server "${serverName}": ignoring zcodeOauth keys the ${type} flow does not accept: ${ignored.join(", ")}.`);
52138
+ return oauth;
52139
+ }
52140
+ /**
52141
+ * The `zcodeOauth` a server was authored with. `getMcpServers()` strips it, so
52142
+ * the authored server (`rawServerOf`) is consulted first.
52143
+ */
52144
+ function authoredZcodeOauth({ name, config, rawServerOf }) {
52145
+ const raw = rawServerOf?.(name);
52146
+ return isRecord$1(raw) ? raw[ZCODE_OAUTH_CANONICAL_KEY] : config[ZCODE_OAUTH_CANONICAL_KEY];
52147
+ }
52148
+ function isPositiveMs(value) {
52149
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
52150
+ }
52151
+ /**
52152
+ * Resolve ZCode's per-server `timeoutMs` (a positive number of milliseconds):
52153
+ * a valid explicit `timeoutMs` wins, otherwise the canonical `timeout`
52154
+ * (milliseconds, the `gitlabduo-mcp.ts` precedent) is used. A value ZCode
52155
+ * would reject is dropped with a warning instead of making ZCode skip the
52156
+ * server.
52157
+ */
52158
+ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
52159
+ const { timeoutMs, timeout } = config;
52160
+ if (timeoutMs !== void 0) {
52161
+ if (isPositiveMs(timeoutMs)) return timeoutMs;
52162
+ logger?.warn(`ZCode MCP server "${serverName}": ignoring timeoutMs ${JSON.stringify(timeoutMs)}, as it must be a positive number of milliseconds.`);
52163
+ }
52164
+ if (timeout === void 0) return void 0;
52165
+ if (isPositiveMs(timeout)) return timeout;
52166
+ logger?.warn(`ZCode MCP server "${serverName}": dropping timeout ${JSON.stringify(timeout)}, as timeoutMs must be a positive number of milliseconds.`);
52167
+ }
52168
+ /**
52169
+ * The per-server keys ZCode reads on every transport: `timeoutMs`,
52170
+ * `protocolVersion` and the enable toggle (`enableKey`).
52171
+ */
52172
+ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
52173
+ const common = {};
52174
+ const timeoutMs = resolveZcodeTimeoutMs({
52175
+ serverName,
52176
+ config,
52177
+ logger
52178
+ });
52179
+ if (timeoutMs !== void 0) common.timeoutMs = timeoutMs;
52180
+ const { protocolVersion } = config;
52181
+ if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
52182
+ else if (protocolVersion !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping protocolVersion ${JSON.stringify(protocolVersion)}, as ZCode accepts only ${[...ZCODE_PROTOCOL_VERSIONS].map((v) => JSON.stringify(v)).join(", ")}.`);
52183
+ if (config.disabled === true) common[enableKey] = false;
52184
+ return common;
52185
+ }
52186
+ /**
50869
52187
  * Convert canonical rulesync servers to ZCode's native `mcp.servers` shape:
50870
52188
  * stdio servers carry `command`/`args`/`env`, remote servers carry
50871
52189
  * `type` (`http` or `sse`), `url` and optional `headers`. A canonical
@@ -50873,9 +52191,18 @@ function asZcodeRemoteType(stated, url) {
50873
52191
  * plugin's `.mcp.json`, via `enableKey`), which it defaults to `true` when
50874
52192
  * absent.
50875
52193
  *
52194
+ * Every server also carries `timeoutMs` (from the canonical `timeout`, or an
52195
+ * explicit `timeoutMs`) and an allow-listed `protocolVersion`; stdio servers
52196
+ * carry `cwd`, and remote servers an `oauth` built from the namespaced
52197
+ * `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
52198
+ * is read from the authored server through `rawServerOf`. ZCode's docs do not
52199
+ * name these keys, so they follow the 3.14.3/3.15.3 runtime's per-server
52200
+ * schema, which is strict: an unknown key makes ZCode skip the server, which
52201
+ * is why the output stays a whitelist.
52202
+ *
50876
52203
  * @see https://zcode.z.ai/en/docs/mcp-services
50877
52204
  */
50878
- function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
52205
+ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable", rawServerOf }) {
50879
52206
  const result = {};
50880
52207
  for (const [name, config] of Object.entries(mcpServers)) {
50881
52208
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
@@ -50915,6 +52242,16 @@ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
50915
52242
  converted.type = type;
50916
52243
  converted.url = url;
50917
52244
  if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
52245
+ const oauth = toZcodeOauth({
52246
+ serverName: name,
52247
+ value: authoredZcodeOauth({
52248
+ name,
52249
+ config,
52250
+ rawServerOf
52251
+ }),
52252
+ logger
52253
+ });
52254
+ if (oauth !== void 0) converted.oauth = oauth;
50918
52255
  } else {
50919
52256
  const [command, ...args] = resolveLocalMcpCommand(config);
50920
52257
  if (!command) {
@@ -50929,35 +52266,62 @@ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
50929
52266
  converted.command = command;
50930
52267
  converted.args = args;
50931
52268
  if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
52269
+ if (typeof config.cwd === "string" && config.cwd !== "") converted.cwd = config.cwd;
50932
52270
  }
50933
- if (config.disabled === true) converted[enableKey] = false;
50934
- result[name] = converted;
52271
+ result[name] = {
52272
+ ...converted,
52273
+ ...toZcodeCommonKeys({
52274
+ serverName: name,
52275
+ config,
52276
+ enableKey,
52277
+ logger
52278
+ })
52279
+ };
50935
52280
  }
50936
52281
  return result;
50937
52282
  }
50938
52283
  /**
52284
+ * ZCode's per-server enable toggle, in both spellings: the runtime normalizes
52285
+ * the legacy `enable` to `enabled`, and a plugin's `.mcp.json` uses `enabled`.
52286
+ */
52287
+ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
52288
+ /**
50939
52289
  * Convert ZCode's native `mcp.servers` shape back to canonical rulesync
50940
- * servers: `enable: false` maps back to `disabled: true`, and unknown keys pass
50941
- * through untouched so an import keeps whatever a hand-authored entry declared.
50942
- * Generation back out is a whitelist, so only the keys `convertToZcodeFormat`
50943
- * writes are re-emitted.
52290
+ * servers: `enable: false` / `enabled: false` map back to `disabled: true`,
52291
+ * and unknown keys pass through untouched so an import keeps whatever a
52292
+ * hand-authored entry declared. `timeoutMs` is lifted back into the canonical
52293
+ * `timeout` (milliseconds); when the server also states a `timeout`, ZCode
52294
+ * has already dropped that one, so `timeoutMs` replaces it. A ZCode-shaped
52295
+ * `oauth` is imported as the namespaced `zcodeOauth`, so its client secret is
52296
+ * never emitted to another tool. Generation back out is a whitelist, so only
52297
+ * the keys `convertToZcodeFormat` writes are re-emitted.
50944
52298
  *
50945
52299
  * An explicit `enable: true` is dropped rather than mapped to a canonical
50946
52300
  * `disabled: false`: absent already means enabled on both sides, so the flag
50947
52301
  * carries no information, and emitting it would push a redundant `disabled`
50948
- * key into every other tool's generated config.
52302
+ * key into every other tool's generated config. (A canonical `enabled` is
52303
+ * rulesync's own generation filter, so it must never be passed through.)
50949
52304
  */
50950
- function convertFromZcodeFormat({ zcodeServers, enableKey = "enable" }) {
52305
+ function convertFromZcodeFormat({ zcodeServers }) {
50951
52306
  const result = {};
50952
52307
  for (const [name, config] of Object.entries(zcodeServers)) {
50953
52308
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
50954
52309
  const converted = {};
50955
52310
  for (const [key, value] of Object.entries(config)) {
50956
52311
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
50957
- if (key === enableKey) {
52312
+ if (ZCODE_ENABLE_KEYS.has(key)) {
50958
52313
  if (value === false) converted.disabled = true;
50959
52314
  continue;
50960
52315
  }
52316
+ if (key === "timeoutMs" && typeof value === "number") {
52317
+ converted.timeout = value;
52318
+ continue;
52319
+ }
52320
+ if (key === "timeout" && typeof config.timeoutMs === "number") continue;
52321
+ if (key === "oauth" && isZcodeOauth(value)) {
52322
+ converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
52323
+ continue;
52324
+ }
50961
52325
  converted[key] = value;
50962
52326
  }
50963
52327
  result[name] = converted;
@@ -51014,7 +52378,8 @@ var ZcodeMcp = class ZcodeMcp extends ToolMcp {
51014
52378
  const existing = parseZcodeConfig(existingContent, filePath);
51015
52379
  const converted = convertToZcodeFormat({
51016
52380
  mcpServers: rulesyncMcp.getMcpServers(),
51017
- logger
52381
+ logger,
52382
+ rawServerOf: (name) => rulesyncMcp.getRawMcpServer(name)
51018
52383
  });
51019
52384
  const existingMcp = isRecord$1(existing["mcp"]) ? existing["mcp"] : {};
51020
52385
  return new ZcodeMcp({
@@ -51118,7 +52483,8 @@ var ZcodePluginMcp = class ZcodePluginMcp extends ToolMcp {
51118
52483
  const mcpServers = convertToZcodeFormat({
51119
52484
  mcpServers: rulesyncMcp.getMcpServers(),
51120
52485
  logger,
51121
- enableKey: ZCODE_PLUGIN_ENABLE_KEY
52486
+ enableKey: ZCODE_PLUGIN_ENABLE_KEY,
52487
+ rawServerOf: (name) => rulesyncMcp.getRawMcpServer(name)
51122
52488
  });
51123
52489
  return new ZcodePluginMcp({
51124
52490
  outputRoot,
@@ -51129,10 +52495,7 @@ var ZcodePluginMcp = class ZcodePluginMcp extends ToolMcp {
51129
52495
  });
51130
52496
  }
51131
52497
  toRulesyncMcp() {
51132
- const mcpServers = convertFromZcodeFormat({
51133
- zcodeServers: serversOf(this.json),
51134
- enableKey: ZCODE_PLUGIN_ENABLE_KEY
51135
- });
52498
+ const mcpServers = convertFromZcodeFormat({ zcodeServers: serversOf(this.json) });
51136
52499
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
51137
52500
  }
51138
52501
  validate() {
@@ -51637,6 +53000,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
51637
53000
  supportsDisabledTools: true
51638
53001
  }
51639
53002
  }],
53003
+ ["openclaw", {
53004
+ class: OpenclawMcp,
53005
+ meta: {
53006
+ supportsProject: false,
53007
+ supportsGlobal: true,
53008
+ supportsEnabledTools: true,
53009
+ supportsDisabledTools: true
53010
+ }
53011
+ }],
51640
53012
  ["kimi-code", {
51641
53013
  class: KimiCodeMcp,
51642
53014
  meta: {
@@ -52790,7 +54162,7 @@ function parsedGlobsIntersect(left, right, budget) {
52790
54162
  *
52791
54163
  * @see https://antigravity.google/docs/permissions?tab=cli
52792
54164
  */
52793
- const REGEX_PREFIX = "regex:";
54165
+ const REGEX_PREFIX$1 = "regex:";
52794
54166
  /**
52795
54167
  * The Antigravity actions whose target is a command: `command`, and
52796
54168
  * `unsandboxed`, which takes the same word-by-word prefix or `regex:` target.
@@ -53128,7 +54500,7 @@ function wordToRegexSource(word) {
53128
54500
  * pattern does.
53129
54501
  */
53130
54502
  function toAntigravityCommandTarget(pattern) {
53131
- if (pattern.startsWith(REGEX_PREFIX)) {
54503
+ if (pattern.startsWith(REGEX_PREFIX$1)) {
53132
54504
  const regexWords = splitWords(pattern.slice(6));
53133
54505
  if (regexWords.length === 0) return { skipReason: "The regex is empty" };
53134
54506
  const invalid = regexWords.find(isInvalidRegex);
@@ -53148,7 +54520,7 @@ function toAntigravityCommandTarget(pattern) {
53148
54520
  const head = words.slice(0, -1);
53149
54521
  if (head.length > 0 && isStarWord(words.at(-1) ?? []) && head.every(isLiteralWord)) return { target: head.map(wordToLiteral).join(" ") };
53150
54522
  return {
53151
- target: `${REGEX_PREFIX}${words.map(wordToRegexSource).join(" ")}`,
54523
+ target: `${REGEX_PREFIX$1}${words.map(wordToRegexSource).join(" ")}`,
53152
54524
  note
53153
54525
  };
53154
54526
  }
@@ -53160,7 +54532,7 @@ function toAntigravityCommandTarget(pattern) {
53160
54532
  * word with a brace may be part of `{a,b}` alternatives, so it ends the prefix.
53161
54533
  */
53162
54534
  function toAntigravityCommandLiteralPrefix(pattern) {
53163
- if (pattern.startsWith(REGEX_PREFIX)) return;
54535
+ if (pattern.startsWith(REGEX_PREFIX$1)) return;
53164
54536
  const words = splitIntoWords(parseGlobPattern(pattern).steps);
53165
54537
  const prefix = [];
53166
54538
  for (const word of words) {
@@ -53168,7 +54540,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
53168
54540
  if (literal === void 0 || literal.includes("{") || literal.includes("}")) break;
53169
54541
  prefix.push(literal);
53170
54542
  }
53171
- if (prefix.length === 0 || prefix[0]?.startsWith(REGEX_PREFIX)) return;
54543
+ if (prefix.length === 0 || prefix[0]?.startsWith(REGEX_PREFIX$1)) return;
53172
54544
  return prefix.join(" ");
53173
54545
  }
53174
54546
  /**
@@ -53219,11 +54591,11 @@ function regexToGlob(source) {
53219
54591
  * `regex:` target is kept as is.
53220
54592
  */
53221
54593
  function fromAntigravityCommandTarget(target) {
53222
- if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
54594
+ if (target.startsWith(REGEX_PREFIX$1)) return regexToGlob(target.slice(6)) ?? target;
53223
54595
  if (target === "*") return target;
53224
54596
  if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
53225
54597
  const words = splitWords(target);
53226
- return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
54598
+ return `${REGEX_PREFIX$1}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
53227
54599
  }
53228
54600
  return `${target} *`;
53229
54601
  }
@@ -53243,7 +54615,7 @@ function fromAntigravityCommandTarget(target) {
53243
54615
  * @see https://antigravity.google/docs/permissions
53244
54616
  */
53245
54617
  function describeAntigravityCommandImportLoss(target) {
53246
- if (target.startsWith(REGEX_PREFIX) || target === "*") return;
54618
+ if (target.startsWith(REGEX_PREFIX$1) || target === "*") return;
53247
54619
  if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
53248
54620
  reason: "it holds characters that are special in a glob (`*`, `?`, `[`, `{` or `\\`), which Antigravity matches literally (likely output of an older rulesync), so it is imported as a literal-match regex that no other tool enforces",
53249
54621
  denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
@@ -55145,7 +56517,7 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
55145
56517
  //#region src/features/permissions/bob-permissions.ts
55146
56518
  const TOOL_LABEL = "IBM Bob";
55147
56519
  /** The top-level settings key holding Bob's auto-approval configuration. */
55148
- const APPROVAL_KEY = "approval";
56520
+ const APPROVAL_KEY$1 = "approval";
55149
56521
  /** The top-level settings key holding Bob's per-feature auto-approve toggles. */
55150
56522
  const AUTO_APPROVE_KEY = "autoApprove";
55151
56523
  /** The tool permission groups Bob documents for `approval.allowed_permissions`. */
@@ -55166,7 +56538,7 @@ const BOB_AUTO_APPROVE_KEYS = /* @__PURE__ */ new Set(["skills"]);
55166
56538
  const OUTSIDE_WORKSPACE_GROUP_ID = "read";
55167
56539
  /** The only executor tool Bob's `allowedExecutors` list documents. */
55168
56540
  const EXECUTE_COMMAND_TOOL_ID = "execute_command";
55169
- const SURFACE_LABEL = `${APPROVAL_KEY}.allowedExecutors[${EXECUTE_COMMAND_TOOL_ID}].approvedCommands/deniedCommands`;
56541
+ const SURFACE_LABEL = `${APPROVAL_KEY$1}.allowedExecutors[${EXECUTE_COMMAND_TOOL_ID}].approvedCommands/deniedCommands`;
55170
56542
  function asStringArray$2(value) {
55171
56543
  if (!Array.isArray(value)) return [];
55172
56544
  return value.filter((entry) => typeof entry === "string");
@@ -55258,7 +56630,7 @@ function warnAboutUndocumentedSettings({ override, logger }) {
55258
56630
  * fetched permissions file. Switches that are already on are not repeated.
55259
56631
  */
55260
56632
  function warnAboutNewAutoApprovals({ settings, override, filePath, logger }) {
55261
- const existingApproval = isPlainObject$1(settings[APPROVAL_KEY]) ? settings[APPROVAL_KEY] : {};
56633
+ const existingApproval = isPlainObject$1(settings[APPROVAL_KEY$1]) ? settings[APPROVAL_KEY$1] : {};
55262
56634
  const existingGroups = new Set(asStringArray$2(existingApproval.allowed_permissions));
55263
56635
  const existingOutside = new Set((Array.isArray(existingApproval.permissionOptions) ? existingApproval.permissionOptions : []).filter(isPlainObject$1).filter((option) => option.enableOutsideWorkspace === true).map((option) => option.groupId));
55264
56636
  const existingAutoApprove = isPlainObject$1(settings[AUTO_APPROVE_KEY]) ? settings[AUTO_APPROVE_KEY] : {};
@@ -55324,7 +56696,7 @@ function buildBobCommandLists({ permission, logger }) {
55324
56696
  */
55325
56697
  function readBobOverride(settings) {
55326
56698
  const override = {};
55327
- const approval = settings[APPROVAL_KEY];
56699
+ const approval = settings[APPROVAL_KEY$1];
55328
56700
  if (isPlainObject$1(approval)) {
55329
56701
  const overrideApproval = {};
55330
56702
  const groups = approval.allowed_permissions;
@@ -55453,8 +56825,8 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
55453
56825
  deniedCommands: denied
55454
56826
  };
55455
56827
  }
55456
- patch[APPROVAL_KEY] = buildApproval({
55457
- existingApproval: settings[APPROVAL_KEY],
56828
+ patch[APPROVAL_KEY$1] = buildApproval({
56829
+ existingApproval: settings[APPROVAL_KEY$1],
55458
56830
  overrideApproval,
55459
56831
  commandLists
55460
56832
  });
@@ -55483,7 +56855,7 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
55483
56855
  fileContent: this.getFileContent(),
55484
56856
  filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
55485
56857
  });
55486
- const approval = settings[APPROVAL_KEY];
56858
+ const approval = settings[APPROVAL_KEY$1];
55487
56859
  const entry = (isPlainObject$1(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
55488
56860
  const rules = {};
55489
56861
  for (const pattern of asStringArray$2(entry?.approvedCommands)) rules[pattern] = "allow";
@@ -57199,7 +58571,7 @@ const CODEBUDDY_RULE_LISTS = [
57199
58571
  "ask",
57200
58572
  "deny"
57201
58573
  ];
57202
- const CATCH_ALL_PATTERN$12 = "*";
58574
+ const CATCH_ALL_PATTERN$13 = "*";
57203
58575
  const MCP_CATEGORY = "mcp";
57204
58576
  const MCP_RULE_PREFIX = "mcp__";
57205
58577
  const ALL_MCP_RULE = "mcp__*";
@@ -57226,7 +58598,7 @@ const CATEGORY_TO_CODEBUDDY_TOOL = {
57226
58598
  skill: "Skill"
57227
58599
  };
57228
58600
  const CODEBUDDY_TOOL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_CODEBUDDY_TOOL).map(([category, tool]) => [tool, category]));
57229
- function toStringList$2(value) {
58601
+ function toStringList$3(value) {
57230
58602
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
57231
58603
  }
57232
58604
  /**
@@ -57272,7 +58644,7 @@ function categoryOfRule$2(rule) {
57272
58644
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
57273
58645
  */
57274
58646
  function buildCodebuddyRule({ category, pattern, action, logger }) {
57275
- const catchAll = pattern === CATCH_ALL_PATTERN$12;
58647
+ const catchAll = pattern === CATCH_ALL_PATTERN$13;
57276
58648
  if (category === MCP_CATEGORY) {
57277
58649
  if (catchAll && action === "allow") {
57278
58650
  logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
@@ -57281,7 +58653,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
57281
58653
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
57282
58654
  }
57283
58655
  if (category === "*") {
57284
- if (catchAll) return CATCH_ALL_PATTERN$12;
58656
+ if (catchAll) return CATCH_ALL_PATTERN$13;
57285
58657
  logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
57286
58658
  return null;
57287
58659
  }
@@ -57299,7 +58671,7 @@ function parseCodebuddyRule(rule) {
57299
58671
  const { specifier } = splitCodebuddyRule(rule);
57300
58672
  if (specifier === void 0) return {
57301
58673
  category,
57302
- pattern: CATCH_ALL_PATTERN$12
58674
+ pattern: CATCH_ALL_PATTERN$13
57303
58675
  };
57304
58676
  const trimmed = specifier.trim();
57305
58677
  return trimmed === "" ? null : {
@@ -57340,7 +58712,7 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
57340
58712
  ask: [],
57341
58713
  deny: []
57342
58714
  };
57343
- for (const list of CODEBUDDY_RULE_LISTS) lists[list] = toStringList$2(existingPermissions[list]).filter((rule) => {
58715
+ for (const list of CODEBUDDY_RULE_LISTS) lists[list] = toStringList$3(existingPermissions[list]).filter((rule) => {
57344
58716
  const category = categoryOfRule$2(rule);
57345
58717
  return category === void 0 || !managedCategories.has(category);
57346
58718
  });
@@ -57431,7 +58803,7 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
57431
58803
  });
57432
58804
  const permissions = isRecord$1(settings[CODEBUDDY_PERMISSIONS_KEY]) ? settings[CODEBUDDY_PERMISSIONS_KEY] : {};
57433
58805
  const permission = {};
57434
- for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringList$2(permissions[action])) {
58806
+ for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringList$3(permissions[action])) {
57435
58807
  const parsed = parseCodebuddyRule(rule);
57436
58808
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
57437
58809
  const rules = permission[parsed.category] ??= {};
@@ -59550,7 +60922,7 @@ function mapBashActionToDecision(action) {
59550
60922
  //#region src/features/permissions/commandcode-permissions.ts
59551
60923
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
59552
60924
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
59553
- const CATCH_ALL_PATTERN$11 = "*";
60925
+ const CATCH_ALL_PATTERN$12 = "*";
59554
60926
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
59555
60927
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
59556
60928
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -59575,14 +60947,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
59575
60947
  web_fetch: "webfetch",
59576
60948
  web_search: "websearch"
59577
60949
  };
59578
- const ACTION_RANK$5 = {
60950
+ const ACTION_RANK$6 = {
59579
60951
  allow: 0,
59580
60952
  ask: 1,
59581
60953
  deny: 2
59582
60954
  };
59583
60955
  /** Whether `action` outranks `existing` (deny > ask > allow). */
59584
60956
  function isStricterAction({ action, existing }) {
59585
- return existing === void 0 || ACTION_RANK$5[action] > ACTION_RANK$5[existing];
60957
+ return existing === void 0 || ACTION_RANK$6[action] > ACTION_RANK$6[existing];
59586
60958
  }
59587
60959
  /**
59588
60960
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -59605,10 +60977,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
59605
60977
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
59606
60978
  */
59607
60979
  function buildCommandcodeRule(category, pattern) {
59608
- const catchAll = pattern === CATCH_ALL_PATTERN$11 || pattern === "";
60980
+ const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
59609
60981
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
59610
60982
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
59611
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$11 : null;
60983
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
59612
60984
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
59613
60985
  if (tool === void 0) return null;
59614
60986
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -59644,7 +61016,7 @@ function splitCommandcodeRule(rule) {
59644
61016
  }
59645
61017
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
59646
61018
  function isCatchAllSpecifier(inner) {
59647
- return inner === "" || inner === CATCH_ALL_PATTERN$11;
61019
+ return inner === "" || inner === CATCH_ALL_PATTERN$12;
59648
61020
  }
59649
61021
  /**
59650
61022
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -59678,34 +61050,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
59678
61050
  if (server.length === 0) return null;
59679
61051
  const everyServer = {
59680
61052
  category: "mcp",
59681
- pattern: CATCH_ALL_PATTERN$11,
61053
+ pattern: CATCH_ALL_PATTERN$12,
59682
61054
  notInAllow: true
59683
61055
  };
59684
61056
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
59685
- if (server === CATCH_ALL_PATTERN$11) return everyServer;
59686
- if (server.includes(CATCH_ALL_PATTERN$11)) return null;
61057
+ if (server === CATCH_ALL_PATTERN$12) return everyServer;
61058
+ if (server.includes(CATCH_ALL_PATTERN$12)) return null;
59687
61059
  return {
59688
- category: rest === "" || rest === CATCH_ALL_PATTERN$11 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
59689
- pattern: CATCH_ALL_PATTERN$11,
61060
+ category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
61061
+ pattern: CATCH_ALL_PATTERN$12,
59690
61062
  notInAllow: false
59691
61063
  };
59692
61064
  }
59693
61065
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
59694
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$11);
61066
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
59695
61067
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
59696
61068
  category,
59697
61069
  pattern: inner,
59698
61070
  notInAllow: true
59699
61071
  } : null;
59700
- if (remainder === CATCH_ALL_PATTERN$11) return everyServer;
59701
- if (rest === CATCH_ALL_PATTERN$11 && !server.includes(CATCH_ALL_PATTERN$11)) return {
61072
+ if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
61073
+ if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
59702
61074
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
59703
- pattern: CATCH_ALL_PATTERN$11,
61075
+ pattern: CATCH_ALL_PATTERN$12,
59704
61076
  notInAllow: true
59705
61077
  };
59706
61078
  return namesTool ? {
59707
61079
  category,
59708
- pattern: CATCH_ALL_PATTERN$11,
61080
+ pattern: CATCH_ALL_PATTERN$12,
59709
61081
  notInAllow: false
59710
61082
  } : null;
59711
61083
  }
@@ -59729,14 +61101,14 @@ function friendlyToolRule({ category, inner }) {
59729
61101
  if (category === "bash") {
59730
61102
  if (isCatchAllSpecifier(inner)) return {
59731
61103
  category,
59732
- pattern: CATCH_ALL_PATTERN$11,
61104
+ pattern: CATCH_ALL_PATTERN$12,
59733
61105
  notInAllow: false
59734
61106
  };
59735
61107
  const normalized = inner.trim().replace(/\s+/g, " ");
59736
61108
  if (normalized === "" || normalized === ":*") return null;
59737
61109
  return isCatchAllSpecifier(normalized) ? {
59738
61110
  category,
59739
- pattern: CATCH_ALL_PATTERN$11,
61111
+ pattern: CATCH_ALL_PATTERN$12,
59740
61112
  notInAllow: true
59741
61113
  } : {
59742
61114
  category,
@@ -59746,7 +61118,7 @@ function friendlyToolRule({ category, inner }) {
59746
61118
  }
59747
61119
  if (isCatchAllSpecifier(inner)) return {
59748
61120
  category,
59749
- pattern: CATCH_ALL_PATTERN$11,
61121
+ pattern: CATCH_ALL_PATTERN$12,
59750
61122
  notInAllow: false
59751
61123
  };
59752
61124
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -59765,9 +61137,9 @@ function friendlyToolRule({ category, inner }) {
59765
61137
  */
59766
61138
  function parseCommandcodeRule(rule) {
59767
61139
  const { tool, inner } = splitCommandcodeRule(rule);
59768
- if (tool === CATCH_ALL_PATTERN$11) return isCatchAllSpecifier(inner) ? {
61140
+ if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
59769
61141
  category: "*",
59770
- pattern: CATCH_ALL_PATTERN$11,
61142
+ pattern: CATCH_ALL_PATTERN$12,
59771
61143
  notInAllow: true
59772
61144
  } : null;
59773
61145
  const lowered = tool.toLowerCase();
@@ -59837,7 +61209,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
59837
61209
  }).filter((rule) => {
59838
61210
  const parsed = parseCommandcodeRule(rule);
59839
61211
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
59840
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$11})`);
61212
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$12})`);
59841
61213
  if (key !== "allow" && isStricterAction({
59842
61214
  action: key,
59843
61215
  existing: replacement
@@ -59894,7 +61266,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
59894
61266
  const ranked = /* @__PURE__ */ new Map();
59895
61267
  const managedCategories = /* @__PURE__ */ new Set();
59896
61268
  for (const [category, rules] of Object.entries(permission)) {
59897
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$11);
61269
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
59898
61270
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
59899
61271
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
59900
61272
  for (const [pattern, action] of Object.entries(rules)) {
@@ -60165,7 +61537,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
60165
61537
  //#endregion
60166
61538
  //#region src/features/permissions/continue-permissions.ts
60167
61539
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
60168
- const CATCH_ALL_PATTERN$10 = "*";
61540
+ const CATCH_ALL_PATTERN$11 = "*";
60169
61541
  /**
60170
61542
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
60171
61543
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -60173,7 +61545,7 @@ const CATCH_ALL_PATTERN$10 = "*";
60173
61545
  * the bare entry instead of being skipped as a pattern-specific rule.
60174
61546
  */
60175
61547
  function normalizeCatchAllPattern$1(pattern) {
60176
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
61548
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
60177
61549
  }
60178
61550
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
60179
61551
  bash: "Bash",
@@ -60210,7 +61582,7 @@ function toCanonicalToolName$6(continueName) {
60210
61582
  * `Tool(pattern)`.
60211
61583
  */
60212
61584
  function buildContinuePermissionEntry(toolName, pattern) {
60213
- return pattern === CATCH_ALL_PATTERN$10 ? toolName : `${toolName}(${pattern})`;
61585
+ return pattern === CATCH_ALL_PATTERN$11 ? toolName : `${toolName}(${pattern})`;
60214
61586
  }
60215
61587
  /**
60216
61588
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -60224,7 +61596,7 @@ function parseContinuePermissionEntry(entry) {
60224
61596
  if (toolName === "") return;
60225
61597
  return {
60226
61598
  toolName,
60227
- pattern: pattern === "" ? CATCH_ALL_PATTERN$10 : pattern
61599
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
60228
61600
  };
60229
61601
  }
60230
61602
  /**
@@ -60398,7 +61770,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
60398
61770
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
60399
61771
  continue;
60400
61772
  }
60401
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$10) {
61773
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
60402
61774
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
60403
61775
  continue;
60404
61776
  }
@@ -60830,7 +62202,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60830
62202
  * arrays are accepted only in MDM/enterprise managed settings, and session
60831
62203
  * tool approvals are machine-written to `permissions-config.json`.
60832
62204
  */
60833
- const WEBFETCH_CATEGORY = "webfetch";
62205
+ const WEBFETCH_CATEGORY$1 = "webfetch";
60834
62206
  /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
60835
62207
  const ALLOWED_URLS_KEY = "allowedUrls";
60836
62208
  const DENIED_URLS_KEY = "deniedUrls";
@@ -60917,7 +62289,7 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
60917
62289
  const paths = CopilotcliPermissions.getSettablePaths({ global });
60918
62290
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
60919
62291
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
60920
- const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
62292
+ const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY$1];
60921
62293
  const patch = {};
60922
62294
  if (rules !== void 0) {
60923
62295
  const { allowedUrls, deniedUrls } = splitUrlRules(rules);
@@ -60945,7 +62317,7 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
60945
62317
  const rules = {};
60946
62318
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
60947
62319
  if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
60948
- const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
62320
+ const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
60949
62321
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
60950
62322
  }
60951
62323
  parseSettings() {
@@ -60980,14 +62352,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
60980
62352
  };
60981
62353
  //#endregion
60982
62354
  //#region src/features/permissions/crush-permissions.ts
60983
- const CATCH_ALL_PATTERN$9 = "*";
62355
+ const CATCH_ALL_PATTERN$10 = "*";
60984
62356
  /**
60985
62357
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
60986
62358
  * argument patterns at all, so it is the only pattern a rule can carry and
60987
62359
  * still be written.
60988
62360
  */
60989
62361
  function normalizeCatchAllPattern(pattern) {
60990
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
62362
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
60991
62363
  }
60992
62364
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
60993
62365
  bash: "bash",
@@ -61186,7 +62558,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
61186
62558
  for (const [rawPattern, action] of Object.entries(rules)) {
61187
62559
  if (isPrototypePollutionKey(rawPattern)) continue;
61188
62560
  const pattern = normalizeCatchAllPattern(rawPattern);
61189
- if (pattern === CATCH_ALL_PATTERN$9) {
62561
+ if (pattern === CATCH_ALL_PATTERN$10) {
61190
62562
  catchAllAction = action;
61191
62563
  continue;
61192
62564
  }
@@ -61259,11 +62631,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
61259
62631
  };
61260
62632
  for (const entry of disabled) {
61261
62633
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
61262
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] = "deny";
62634
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
61263
62635
  }
61264
62636
  for (const entry of allowed) {
61265
62637
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
61266
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] ??= "allow";
62638
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
61267
62639
  }
61268
62640
  return { permission };
61269
62641
  }
@@ -62705,7 +64077,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
62705
64077
  * (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
62706
64078
  * merge into the existing JSON and the file is never deleted; only the keys
62707
64079
  * this feature manages are rewritten — `permissions`, plus `sandbox` in global
62708
- * mode when the `devin` override authors it.
64080
+ * mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
64081
+ * `forbiddenPlugins`) in project mode when the `devin` override authors them.
62709
64082
  *
62710
64083
  * The sibling `sandbox` block — which decides what a permitted command may
62711
64084
  * reach rather than which commands are permitted — has no canonical category
@@ -62726,7 +64099,8 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62726
64099
  /**
62727
64100
  * config.json may carry the MCP/hooks features' keys, so it is never deleted;
62728
64101
  * only the keys this feature manages are rewritten — `permissions`, plus
62729
- * `sandbox` in global mode when the `devin` override authors it.
64102
+ * `sandbox` (global) and the plugin governance lists (project) when the
64103
+ * `devin` override authors them.
62730
64104
  */
62731
64105
  isDeletable() {
62732
64106
  return false;
@@ -62821,6 +64195,13 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62821
64195
  });
62822
64196
  } else if (Object.keys(authoredSandboxRecord).length > 0) logger?.warn("Devin reads 'sandbox' from the user config only, so the 'devin.sandbox' override was dropped from the project config. Generate with --global to author it.");
62823
64197
  }
64198
+ applyPluginGovernanceOverride({
64199
+ override: config.devin,
64200
+ settings,
64201
+ patch,
64202
+ global,
64203
+ logger
64204
+ });
62824
64205
  return new DevinPermissions({
62825
64206
  outputRoot,
62826
64207
  relativeDirPath: paths.relativeDirPath,
@@ -62835,7 +64216,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62835
64216
  validate
62836
64217
  });
62837
64218
  }
62838
- toRulesyncPermissions() {
64219
+ toRulesyncPermissions({ logger } = {}) {
62839
64220
  let settings;
62840
64221
  try {
62841
64222
  const parsed = JSON.parse(this.getFileContent());
@@ -62850,8 +64231,16 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62850
64231
  deny: Array.isArray(permissions.deny) ? permissions.deny : []
62851
64232
  });
62852
64233
  const sandbox = asDevinRecord(settings.sandbox);
64234
+ const devinOverride = {};
64235
+ if (Object.keys(sandbox).length > 0) devinOverride.sandbox = sandbox;
64236
+ for (const key of DEVIN_PLUGIN_GOVERNANCE_KEYS) {
64237
+ const value = settings[key];
64238
+ if (value === void 0) continue;
64239
+ if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
64240
+ else logger?.warn(`Skipped Devin \`${key}\` on import: ${key === "forbiddenPlugins" ? "expected an array of strings" : "expected an array of plugin sources (strings or objects)"}.`);
64241
+ }
62853
64242
  const result = { ...config };
62854
- if (Object.keys(sandbox).length > 0) result.devin = { sandbox };
64243
+ if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
62855
64244
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
62856
64245
  }
62857
64246
  validate() {
@@ -62871,6 +64260,42 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62871
64260
  }
62872
64261
  };
62873
64262
  /**
64263
+ * The repo-level plugin governance lists of a checkout's `.devin/config.json`.
64264
+ *
64265
+ * @see https://docs.devin.ai/cli/extensibility/plugins/overview
64266
+ */
64267
+ const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
64268
+ "requiredPlugins",
64269
+ "optionalPlugins",
64270
+ "forbiddenPlugins"
64271
+ ];
64272
+ /**
64273
+ * Patch the plugin governance lists the `devin` override authors into the
64274
+ * project `.devin/config.json`. Each authored list replaces the file's list
64275
+ * whole; a list it does not author is left as the file holds it. Devin reads these
64276
+ * lists only from a checkout's `.devin/config.json`, so in global mode they are
64277
+ * dropped with a warning. Losing a `forbiddenPlugins` entry the file held is
64278
+ * announced, since that lifts a restriction. A non-empty `requiredPlugins` that changes the file
64279
+ * is announced, since Devin installs required plugins automatically and a
64280
+ * plugin can ship its own hooks and MCP servers.
64281
+ */
64282
+ function applyPluginGovernanceOverride({ override, settings, patch, global, logger }) {
64283
+ const authoredKeys = DEVIN_PLUGIN_GOVERNANCE_KEYS.filter((key) => override?.[key] !== void 0);
64284
+ if (authoredKeys.length === 0) return;
64285
+ if (global) {
64286
+ logger?.warn(`Devin reads the plugin governance lists (${authoredKeys.join(", ")}) from a checkout's '.devin/config.json' only, so they were dropped from the user config. Generate without --global to author them.`);
64287
+ return;
64288
+ }
64289
+ for (const key of authoredKeys) patch[key] = override?.[key];
64290
+ const forbidden = override?.forbiddenPlugins;
64291
+ if (Array.isArray(forbidden) && Array.isArray(settings.forbiddenPlugins)) {
64292
+ const lost = settings.forbiddenPlugins.filter((entry) => !forbidden.includes(entry));
64293
+ if (lost.length > 0) logger?.warn(`Replacing Devin \`forbiddenPlugins\` drops ${lost.map(quoteValueForWarning).join(", ")} from '.devin/config.json': those plugins are no longer forbidden at the repo level.`);
64294
+ }
64295
+ const required = override?.requiredPlugins;
64296
+ if (Array.isArray(required) && required.length > 0 && !(0, node_util.isDeepStrictEqual)(required, settings.requiredPlugins)) logger?.warn(`Writing Devin \`requiredPlugins\` (${required.map(quoteValueForWarning).join(", ")}): Devin installs required plugins automatically, and a plugin can ship its own hooks and MCP servers.`);
64297
+ }
64298
+ /**
62874
64299
  * Collapse the pattern rules of a bare-only scope to the single action Devin
62875
64300
  * can hold for it, using deny > ask > allow precedence. A map without a
62876
64301
  * catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
@@ -63128,7 +64553,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
63128
64553
  //#region src/features/permissions/goose-permissions.ts
63129
64554
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
63130
64555
  const GOOSE_USER_KEY = "user";
63131
- const CATCH_ALL_PATTERN$8 = "*";
64556
+ const CATCH_ALL_PATTERN$9 = "*";
63132
64557
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
63133
64558
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
63134
64559
  shell: ["bash"],
@@ -63140,7 +64565,7 @@ const ACTION_TO_GOOSE_LIST = {
63140
64565
  ask: "ask_before",
63141
64566
  deny: "never_allow"
63142
64567
  };
63143
- const ACTION_RANK$4 = {
64568
+ const ACTION_RANK$5 = {
63144
64569
  allow: 0,
63145
64570
  ask: 1,
63146
64571
  deny: 2
@@ -63292,14 +64717,14 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
63292
64717
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
63293
64718
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
63294
64719
  for (const [pattern, action] of Object.entries(rules)) {
63295
- if (pattern !== CATCH_ALL_PATTERN$8) {
64720
+ if (pattern !== CATCH_ALL_PATTERN$9) {
63296
64721
  logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
63297
64722
  continue;
63298
64723
  }
63299
64724
  const previous = assigned.get(toolName);
63300
64725
  if (previous === action) continue;
63301
64726
  if (previous !== void 0) {
63302
- const stricter = ACTION_RANK$4[action] > ACTION_RANK$4[previous] ? action : previous;
64727
+ const stricter = ACTION_RANK$5[action] > ACTION_RANK$5[previous] ? action : previous;
63303
64728
  logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
63304
64729
  if (stricter === previous) continue;
63305
64730
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
@@ -63323,7 +64748,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
63323
64748
  const action = GOOSE_LIST_TO_ACTION[key];
63324
64749
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
63325
64750
  permission[category] ??= {};
63326
- permission[category][CATCH_ALL_PATTERN$8] = action;
64751
+ permission[category][CATCH_ALL_PATTERN$9] = action;
63327
64752
  }
63328
64753
  }
63329
64754
  return { permission };
@@ -63334,7 +64759,7 @@ const GROKCLI_UI_KEY = "ui";
63334
64759
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
63335
64760
  const GROKCLI_PERMISSION_KEY = "permission";
63336
64761
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
63337
- const CATCH_ALL_PATTERN$7 = "*";
64762
+ const CATCH_ALL_PATTERN$8 = "*";
63338
64763
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
63339
64764
  const CATEGORY_TO_GROK_TOOL = {
63340
64765
  bash: "Bash",
@@ -63380,15 +64805,15 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
63380
64805
  * under it has no Grok spelling and returns `null`.
63381
64806
  */
63382
64807
  function buildGrokEntry(category, pattern) {
63383
- if (category === "*") return pattern === CATCH_ALL_PATTERN$7 ? GROK_ALL_TOOLS_ENTRY : null;
64808
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
63384
64809
  if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
63385
64810
  const remainder = category.slice(5);
63386
64811
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
63387
64812
  }
63388
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
64813
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
63389
64814
  const tool = CATEGORY_TO_GROK_TOOL[category];
63390
64815
  if (tool === void 0) return null;
63391
- return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? tool : `${tool}(${pattern})`;
64816
+ return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
63392
64817
  }
63393
64818
  /**
63394
64819
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -63410,21 +64835,21 @@ function parseGrokEntry(entry) {
63410
64835
  }
63411
64836
  if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
63412
64837
  category: "*",
63413
- pattern: CATCH_ALL_PATTERN$7
64838
+ pattern: CATCH_ALL_PATTERN$8
63414
64839
  };
63415
64840
  if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$3)) return parseClaudeStyleMcpEntry(tool);
63416
64841
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
63417
64842
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
63418
- pattern: CATCH_ALL_PATTERN$7
64843
+ pattern: CATCH_ALL_PATTERN$8
63419
64844
  } : {
63420
64845
  category: "mcp",
63421
- pattern: CATCH_ALL_PATTERN$7
64846
+ pattern: CATCH_ALL_PATTERN$8
63422
64847
  };
63423
64848
  const category = GROK_TOOL_TO_CATEGORY[tool];
63424
64849
  if (category === void 0) return null;
63425
64850
  return {
63426
64851
  category,
63427
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$7
64852
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
63428
64853
  };
63429
64854
  }
63430
64855
  /**
@@ -63441,14 +64866,14 @@ function parseGrokEntry(entry) {
63441
64866
  function parseClaudeStyleMcpEntry(entry) {
63442
64867
  const rest = entry.slice(5);
63443
64868
  if (rest.length === 0) return null;
63444
- if (rest === CATCH_ALL_PATTERN$7) return {
64869
+ if (rest === CATCH_ALL_PATTERN$8) return {
63445
64870
  category: "mcp",
63446
- pattern: CATCH_ALL_PATTERN$7
64871
+ pattern: CATCH_ALL_PATTERN$8
63447
64872
  };
63448
64873
  const address = rest.includes("__") ? rest : `${rest}__*`;
63449
64874
  return {
63450
64875
  category: `${MCP_CANONICAL_PREFIX$3}${address}`,
63451
- pattern: CATCH_ALL_PATTERN$7
64876
+ pattern: CATCH_ALL_PATTERN$8
63452
64877
  };
63453
64878
  }
63454
64879
  /**
@@ -63469,20 +64894,20 @@ function parseGrokRule(rule) {
63469
64894
  if (typeof tool !== "string") return null;
63470
64895
  if (pattern !== void 0 && typeof pattern !== "string") return null;
63471
64896
  const trimmedPattern = pattern?.trim() ?? "";
63472
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$7;
64897
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
63473
64898
  const lowerTool = tool.trim().toLowerCase();
63474
- if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
64899
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
63475
64900
  category: "*",
63476
- pattern: CATCH_ALL_PATTERN$7,
64901
+ pattern: CATCH_ALL_PATTERN$8,
63477
64902
  action
63478
64903
  } : null;
63479
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
64904
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
63480
64905
  category: "mcp",
63481
- pattern: CATCH_ALL_PATTERN$7,
64906
+ pattern: CATCH_ALL_PATTERN$8,
63482
64907
  action
63483
64908
  } : {
63484
64909
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
63485
- pattern: CATCH_ALL_PATTERN$7,
64910
+ pattern: CATCH_ALL_PATTERN$8,
63486
64911
  action
63487
64912
  };
63488
64913
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -63635,7 +65060,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
63635
65060
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
63636
65061
  }
63637
65062
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
63638
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$7]: legacyModeAction(parsed) } } };
65063
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$8]: legacyModeAction(parsed) } } };
63639
65064
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
63640
65065
  }
63641
65066
  validate() {
@@ -63655,7 +65080,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
63655
65080
  });
63656
65081
  }
63657
65082
  };
63658
- const ACTION_RANK$3 = {
65083
+ const ACTION_RANK$4 = {
63659
65084
  allow: 0,
63660
65085
  ask: 1,
63661
65086
  deny: 2
@@ -63690,7 +65115,7 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
63690
65115
  }
63691
65116
  const existing = ranked.get(entry);
63692
65117
  if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
63693
- if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) ranked.set(entry, action);
65118
+ if (existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing]) ranked.set(entry, action);
63694
65119
  }
63695
65120
  warnRemovedAllToolsRestrictions({
63696
65121
  existingPermission,
@@ -63726,7 +65151,7 @@ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger })
63726
65151
  const entries = existingPermission[action];
63727
65152
  if (!isStringArray$1(entries)) continue;
63728
65153
  if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63729
- if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
65154
+ if (generated !== void 0 && ACTION_RANK$4[generated] >= ACTION_RANK$4[action]) continue;
63730
65155
  logger.warn(`Grok CLI permissions: removing the existing bare '*' entry from the '${action}' array — Rulesync now manages it through the all-tools '*' category, and the canonical config has no '*' '${action}' rule. Add "*": { "*": "${action}" } to keep restricting every tool.`);
63731
65156
  }
63732
65157
  }
@@ -63741,7 +65166,7 @@ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63741
65166
  if (!logger) return;
63742
65167
  const allTools = ranked.get(GROK_ALL_TOOLS_ENTRY);
63743
65168
  if (allTools === void 0 || allTools === "allow") return;
63744
- const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$3[action] < ACTION_RANK$3[allTools]).map(([entry]) => entry).toSorted();
65169
+ const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$4[action] < ACTION_RANK$4[allTools]).map(([entry]) => entry).toSorted();
63745
65170
  if (shadowed.length === 0) return;
63746
65171
  logger.warn(`Grok CLI permissions: the bare '*' '${allTools}' rule applies to every tool, and Grok evaluates deny > ask > allow, so these less strict entries never take effect: ${shadowed.join(", ")}.`);
63747
65172
  }
@@ -63764,7 +65189,7 @@ function parseGrokPermissionArrays(permission) {
63764
65189
  const record = ({ category, pattern, action }) => {
63765
65190
  const bucket = result[category] ??= {};
63766
65191
  const existing = bucket[pattern];
63767
- if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) bucket[pattern] = action;
65192
+ if (existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing]) bucket[pattern] = action;
63768
65193
  };
63769
65194
  const applyEntries = (entries, action) => {
63770
65195
  for (const entry of entries ?? []) {
@@ -64168,7 +65593,7 @@ const JUNIE_GROUP_TO_CANONICAL = {
64168
65593
  mcpTools: "mcp",
64169
65594
  readOutsideProject: "read"
64170
65595
  };
64171
- function isPermissionAction$2(value) {
65596
+ function isPermissionAction$3(value) {
64172
65597
  return PermissionActionSchema.safeParse(value).success;
64173
65598
  }
64174
65599
  /**
@@ -64354,7 +65779,7 @@ function convertJunieToRulesyncPermissions({ allowlist }) {
64354
65779
  for (const rule of Array.isArray(ruleSet.rules) ? ruleSet.rules : []) {
64355
65780
  if (!rule || typeof rule !== "object") continue;
64356
65781
  const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
64357
- if (pattern === void 0 || !isPermissionAction$2(rule.action)) continue;
65782
+ if (pattern === void 0 || !isPermissionAction$3(rule.action)) continue;
64358
65783
  (permission[category] ??= {})[pattern] = rule.action;
64359
65784
  }
64360
65785
  }
@@ -65314,7 +66739,7 @@ function rulesFromArrays(settings, allowKey, denyKey) {
65314
66739
  }
65315
66740
  //#endregion
65316
66741
  //#region src/features/permissions/kiro-capability-permissions.ts
65317
- const CATCH_ALL_PATTERN$6 = "*";
66742
+ const CATCH_ALL_PATTERN$7 = "*";
65318
66743
  const CANONICAL_TO_KIRO_CAPABILITY = {
65319
66744
  bash: "shell",
65320
66745
  read: "fs_read",
@@ -65331,12 +66756,12 @@ const KIRO_CAPABILITY_TO_CANONICAL = {
65331
66756
  web_search: "websearch"
65332
66757
  };
65333
66758
  const MANAGED_CAPABILITIES = new Set(Object.keys(KIRO_CAPABILITY_TO_CANONICAL));
65334
- const ACTION_RANK$2 = {
66759
+ const ACTION_RANK$3 = {
65335
66760
  allow: 0,
65336
66761
  ask: 1,
65337
66762
  deny: 2
65338
66763
  };
65339
- function isPermissionAction$1(value) {
66764
+ function isPermissionAction$2(value) {
65340
66765
  return value === "allow" || value === "ask" || value === "deny";
65341
66766
  }
65342
66767
  /**
@@ -65363,7 +66788,7 @@ function rulePatternKey(capability, pattern) {
65363
66788
  function classifyExistingEntry({ emitted, capability, pattern, effect }) {
65364
66789
  const effects = emitted.get(rulePatternKey(capability, pattern));
65365
66790
  if (effects === void 0) return "unrelated";
65366
- return isPermissionAction$1(effect) && effects.has(effect) ? "duplicate" : "conflict";
66791
+ return isPermissionAction$2(effect) && effects.has(effect) ? "duplicate" : "conflict";
65367
66792
  }
65368
66793
  /**
65369
66794
  * Merge one existing rule against the generated entries (see
@@ -65378,7 +66803,7 @@ function mergeExistingRule({ rule, emitted, logger }) {
65378
66803
  };
65379
66804
  const capability = rule.capability;
65380
66805
  const isCatchAll = rule.match === void 0;
65381
- const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
66806
+ const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$7];
65382
66807
  if (patterns.length === 0) return {
65383
66808
  rule,
65384
66809
  unrelatedEntries: 0
@@ -65432,7 +66857,7 @@ function mergeExistingRule({ rule, emitted, logger }) {
65432
66857
  */
65433
66858
  function mergeKiroCapabilityRules({ existingRules, generatedRules, logger }) {
65434
66859
  const emitted = /* @__PURE__ */ new Map();
65435
- for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$6]) {
66860
+ for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$7]) {
65436
66861
  const key = rulePatternKey(rule.capability, pattern);
65437
66862
  const effects = emitted.get(key) ?? /* @__PURE__ */ new Set();
65438
66863
  effects.add(rule.effect);
@@ -65560,13 +66985,13 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
65560
66985
  const rules = isRecord$1(parsed) && Array.isArray(parsed.rules) ? parsed.rules : [];
65561
66986
  const permission = {};
65562
66987
  for (const rule of rules) {
65563
- if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
66988
+ if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$2(rule.effect)) continue;
65564
66989
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
65565
- const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$1(rule.match) ? rule.match : [];
66990
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$7] : isStringArray$1(rule.match) ? rule.match : [];
65566
66991
  const categoryRules = permission[category] ??= {};
65567
66992
  for (const pattern of patterns) {
65568
66993
  const previous = categoryRules[pattern];
65569
- if (previous === void 0 || ACTION_RANK$2[rule.effect] > ACTION_RANK$2[previous]) categoryRules[pattern] = rule.effect;
66994
+ if (previous === void 0 || ACTION_RANK$3[rule.effect] > ACTION_RANK$3[previous]) categoryRules[pattern] = rule.effect;
65570
66995
  }
65571
66996
  }
65572
66997
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
@@ -65620,11 +67045,11 @@ function buildKiroCapabilityRules({ config, logger }) {
65620
67045
  }
65621
67046
  const result = [];
65622
67047
  for (const { capability, effect, patterns } of buckets.values()) {
65623
- if (patterns.has(CATCH_ALL_PATTERN$6)) result.push({
67048
+ if (patterns.has(CATCH_ALL_PATTERN$7)) result.push({
65624
67049
  capability,
65625
67050
  effect
65626
67051
  });
65627
- const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$6);
67052
+ const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$7);
65628
67053
  if (match.length > 0) result.push({
65629
67054
  capability,
65630
67055
  match,
@@ -65649,7 +67074,7 @@ function serializeKiroPermissions({ next, parsed, existingContent }) {
65649
67074
  */
65650
67075
  function warnOnWorkspaceRelativeFsPattern({ capability, category, pattern, logger }) {
65651
67076
  if (capability !== "fs_read" && capability !== "fs_write") return;
65652
- if (pattern === CATCH_ALL_PATTERN$6 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
67077
+ if (pattern === CATCH_ALL_PATTERN$7 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
65653
67078
  logger?.warn(`Kiro permissions.yaml is user-level and applies to every workspace, but the ${category} pattern "${pattern}" is relative. Use an absolute, ~-relative or **-prefixed pattern to target a specific location.`);
65654
67079
  }
65655
67080
  /**
@@ -65677,7 +67102,7 @@ const KiroScopedPermissions = {
65677
67102
  //#region src/features/permissions/lettacode-permissions.ts
65678
67103
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
65679
67104
  const LETTACODE_PERMISSIONS_KEY = "permissions";
65680
- const CATCH_ALL_PATTERN$5 = "*";
67105
+ const CATCH_ALL_PATTERN$6 = "*";
65681
67106
  /** Letta Code's `Bash(prefix:*)` prefix-match suffix. */
65682
67107
  const PREFIX_SUFFIX = ":*";
65683
67108
  const CATEGORY_TO_LETTACODE_TOOL = {
@@ -65722,12 +67147,12 @@ const LIST_TO_ACTION = {
65722
67147
  alwaysAsk: "ask",
65723
67148
  deny: "deny"
65724
67149
  };
65725
- const ACTION_RANK$1 = {
67150
+ const ACTION_RANK$2 = {
65726
67151
  allow: 0,
65727
67152
  ask: 1,
65728
67153
  deny: 2
65729
67154
  };
65730
- function toStringList$1(value) {
67155
+ function toStringList$2(value) {
65731
67156
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65732
67157
  }
65733
67158
  /**
@@ -65770,7 +67195,7 @@ function categoryOfRule$1(rule) {
65770
67195
  * widening it would grant commands the source never allowed.
65771
67196
  */
65772
67197
  function toLettacodeBashPayload({ pattern, action, logger }) {
65773
- if (pattern === CATCH_ALL_PATTERN$5) return "";
67198
+ if (pattern === CATCH_ALL_PATTERN$6) return "";
65774
67199
  const firstStar = pattern.indexOf("*");
65775
67200
  if (firstStar === -1) return pattern;
65776
67201
  if (pattern.endsWith(PREFIX_SUFFIX) && firstStar === pattern.length - 1) return pattern;
@@ -65791,7 +67216,7 @@ function toLettacodeRule({ category, pattern, action, logger }) {
65791
67216
  pattern,
65792
67217
  action,
65793
67218
  logger
65794
- }) : pattern === CATCH_ALL_PATTERN$5 ? "" : pattern;
67219
+ }) : pattern === CATCH_ALL_PATTERN$6 ? "" : pattern;
65795
67220
  if (payload === null) return null;
65796
67221
  return payload === "" ? tool : `${tool}(${payload})`;
65797
67222
  }
@@ -65802,7 +67227,7 @@ function fromLettacodeRule(rule) {
65802
67227
  const { payload } = splitLettacodeRule(rule);
65803
67228
  if (payload === void 0) return {
65804
67229
  category,
65805
- pattern: CATCH_ALL_PATTERN$5
67230
+ pattern: CATCH_ALL_PATTERN$6
65806
67231
  };
65807
67232
  const trimmed = payload.trim();
65808
67233
  if (trimmed === "") return null;
@@ -65810,7 +67235,7 @@ function fromLettacodeRule(rule) {
65810
67235
  const prefix = trimmed.slice(0, -2).trimEnd();
65811
67236
  return {
65812
67237
  category,
65813
- pattern: prefix === "" ? CATCH_ALL_PATTERN$5 : `${prefix} *`
67238
+ pattern: prefix === "" ? CATCH_ALL_PATTERN$6 : `${prefix} *`
65814
67239
  };
65815
67240
  }
65816
67241
  return {
@@ -65845,11 +67270,11 @@ function buildLettacodeRuleLists({ config, existingPermissions, logger }) {
65845
67270
  });
65846
67271
  if (rule === null) continue;
65847
67272
  const existing = ranked.get(rule);
65848
- if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) ranked.set(rule, action);
67273
+ if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) ranked.set(rule, action);
65849
67274
  }
65850
67275
  }
65851
67276
  const lists = {};
65852
- for (const key of Object.keys(LIST_TO_ACTION)) lists[key] = toStringList$1(existingPermissions[key]).filter((rule) => {
67277
+ for (const key of Object.keys(LIST_TO_ACTION)) lists[key] = toStringList$2(existingPermissions[key]).filter((rule) => {
65853
67278
  const category = categoryOfRule$1(rule);
65854
67279
  return category === void 0 || !managedCategories.has(category);
65855
67280
  });
@@ -65938,12 +67363,12 @@ var LettacodePermissions = class LettacodePermissions extends ToolPermissions {
65938
67363
  });
65939
67364
  const permissions = isRecord$1(settings[LETTACODE_PERMISSIONS_KEY]) ? settings[LETTACODE_PERMISSIONS_KEY] : {};
65940
67365
  const permission = {};
65941
- for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringList$1(permissions[key])) {
67366
+ for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringList$2(permissions[key])) {
65942
67367
  const parsed = fromLettacodeRule(rule);
65943
67368
  if (parsed === null || isPrototypePollutionKey(parsed.pattern)) continue;
65944
67369
  const rules = permission[parsed.category] ??= {};
65945
67370
  const existing = rules[parsed.pattern];
65946
- if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) rules[parsed.pattern] = action;
67371
+ if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) rules[parsed.pattern] = action;
65947
67372
  }
65948
67373
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
65949
67374
  }
@@ -66127,7 +67552,11 @@ var OpencodePermissions = class extends ToolPermissions {
66127
67552
  fileContent = await readFileContentOrNull(jsonPath);
66128
67553
  if (fileContent) relativeFilePath = this.layout.jsonFileName;
66129
67554
  }
66130
- const parsed = (0, jsonc_parser.parse)(fileContent ?? "{}");
67555
+ const parsed = (await readOpencodeDotDirConfigLayers({
67556
+ outputRoot,
67557
+ global,
67558
+ layout: this.layout
67559
+ })).map(({ config }) => config).reduce(mergeOpencodeConfigValues, (0, jsonc_parser.parse)(fileContent ?? "{}"));
66131
67560
  const record = isRecord$1(parsed) ? parsed : {};
66132
67561
  const nextJson = {
66133
67562
  ...record,
@@ -66178,6 +67607,16 @@ var OpencodePermissions = class extends ToolPermissions {
66178
67607
  value,
66179
67608
  logger
66180
67609
  });
67610
+ warnOpencodeDotDirOverrides({
67611
+ generated: { permission },
67612
+ overlays: await readOpencodeDotDirConfigLayers({
67613
+ outputRoot,
67614
+ global,
67615
+ layout: this.layout
67616
+ }),
67617
+ rootFileName: relativeFilePath,
67618
+ logger
67619
+ });
66181
67620
  return new this({
66182
67621
  outputRoot,
66183
67622
  relativeDirPath: basePaths.relativeDirPath,
@@ -66267,7 +67706,7 @@ var MimocodePermissions = class extends OpencodePermissions {
66267
67706
  };
66268
67707
  //#endregion
66269
67708
  //#region src/features/permissions/omp-permissions.ts
66270
- const CATCH_ALL_PATTERN$4 = "*";
67709
+ const CATCH_ALL_PATTERN$5 = "*";
66271
67710
  const APPROVAL_MODE_KEY = "approvalMode";
66272
67711
  const ACTION_TO_OMP_APPROVAL = {
66273
67712
  allow: "allow",
@@ -66464,7 +67903,7 @@ function allToolsFloor(rules) {
66464
67903
  let patternSpecific = false;
66465
67904
  for (const [pattern, action] of Object.entries(rules ?? {})) {
66466
67905
  if (action === "allow" || isPrototypePollutionKey(pattern)) continue;
66467
- if (pattern === CATCH_ALL_PATTERN$4) floor = stricter(floor, action);
67906
+ if (pattern === CATCH_ALL_PATTERN$5) floor = stricter(floor, action);
66468
67907
  else {
66469
67908
  patternSpecific = true;
66470
67909
  floor = stricter(floor, "ask");
@@ -66499,11 +67938,11 @@ function convertBashRules$1({ rules, logger }) {
66499
67938
  const entries = Object.entries(rules).filter(([pattern]) => !isPrototypePollutionKey(pattern));
66500
67939
  const hasAsk = entries.some(([, action]) => action === "ask");
66501
67940
  let policy = hasAsk ? "ask" : void 0;
66502
- if (hasAsk && rules[CATCH_ALL_PATTERN$4] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
67941
+ if (hasAsk && rules[CATCH_ALL_PATTERN$5] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
66503
67942
  for (const [pattern, action] of entries) {
66504
- if (pattern === CATCH_ALL_PATTERN$4 && action === "allow") {
67943
+ if (pattern === CATCH_ALL_PATTERN$5 && action === "allow") {
66505
67944
  if (hasAsk) patterns.push({
66506
- match: CATCH_ALL_PATTERN$4,
67945
+ match: CATCH_ALL_PATTERN$5,
66507
67946
  approval: "allow"
66508
67947
  });
66509
67948
  else policy = "allow";
@@ -66606,12 +68045,12 @@ function toOmpApproval(value) {
66606
68045
  * ask rules (see `convertBashRules`), so it adds no catch-all.
66607
68046
  */
66608
68047
  function importBashPolicy(bashRules, action) {
66609
- const catchAll = bashRules[CATCH_ALL_PATTERN$4];
68048
+ const catchAll = bashRules[CATCH_ALL_PATTERN$5];
66610
68049
  const hasAskPattern = Object.values(bashRules).includes("ask");
66611
- if (action === "deny") bashRules[CATCH_ALL_PATTERN$4] = "deny";
68050
+ if (action === "deny") bashRules[CATCH_ALL_PATTERN$5] = "deny";
66612
68051
  else if (catchAll === void 0) {
66613
- if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = action;
66614
- } else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = "ask";
68052
+ if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$5] = action;
68053
+ } else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$5] = "ask";
66615
68054
  }
66616
68055
  /** `bash.patterns` as canonical `bash` rules, in first-match order. */
66617
68056
  function importBashPatterns(value) {
@@ -66639,7 +68078,7 @@ function convertOmpToRulesync(config) {
66639
68078
  const action = OMP_APPROVAL_TO_ACTION[approval];
66640
68079
  if (tool === "bash") continue;
66641
68080
  const category = renamed(OMP_TOOL_TO_CATEGORY, tool);
66642
- permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
68081
+ permission[category] = { [CATCH_ALL_PATTERN$5]: stricter(permission[category]?.[CATCH_ALL_PATTERN$5], action) };
66643
68082
  }
66644
68083
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
66645
68084
  const approvalMode = tools[APPROVAL_MODE_KEY];
@@ -66769,7 +68208,7 @@ const POOL_DENY_KEY = "deny";
66769
68208
  const POOL_DISABLED_KEY = "disabled";
66770
68209
  const POOL_PATH_KEY = "path";
66771
68210
  const POOL_WRITE_KEY = "write";
66772
- const CATCH_ALL_PATTERN$3 = "*";
68211
+ const CATCH_ALL_PATTERN$4 = "*";
66773
68212
  const PATHS_CATCH_ALL_PATTERN = "**";
66774
68213
  const CANONICAL_TO_POOL_TOOL_NAMES = {
66775
68214
  bash: "shell",
@@ -67277,7 +68716,7 @@ function pathOfEntry(entry) {
67277
68716
  */
67278
68717
  function importPoolTool({ toolConfig, bucket }) {
67279
68718
  if (toolConfig[POOL_DISABLED_KEY] === true) {
67280
- bucket[CATCH_ALL_PATTERN$3] = "deny";
68719
+ bucket[CATCH_ALL_PATTERN$4] = "deny";
67281
68720
  return;
67282
68721
  }
67283
68722
  const denied = isStringArray$1(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
@@ -67334,9 +68773,58 @@ const QODER_PERMISSIONS_KEY = "permissions";
67334
68773
  * @see https://docs.qoder.com/cli/settings-reference
67335
68774
  */
67336
68775
  const QODER_OUTPUT_STYLE_KEY = "outputStyle";
67337
- const CATCH_ALL_PATTERN$2 = "*";
68776
+ /**
68777
+ * The settings groups the `qoder` override may author, each with the only keys
68778
+ * of that group rulesync writes and imports. A group's other keys are kept as
68779
+ * the file has them; an override key not listed here is skipped with a
68780
+ * warning, so a shared permissions file cannot reach `tools.shell.pager`,
68781
+ * `general.preferredEditor` and the like.
68782
+ * @see https://docs.qoder.com/cli/settings-reference
68783
+ */
68784
+ const QODER_OVERRIDE_GROUP_KEYS = {
68785
+ general: ["defaultPermissionMode"],
68786
+ tools: ["core", "exclude"],
68787
+ mcp: [
68788
+ "allowed",
68789
+ "excluded",
68790
+ "enableAllProjectMcpServers",
68791
+ "enabledProjectMcpServers",
68792
+ "lazyLoad"
68793
+ ],
68794
+ agents: ["overrides"]
68795
+ };
68796
+ const QODER_OVERRIDE_GROUPS = Object.keys(QODER_OVERRIDE_GROUP_KEYS);
68797
+ /**
68798
+ * Top-level Auto-mode classifier guidance. Qoder reads it from the user
68799
+ * settings only (a workspace file could otherwise bias the classifier), so it
68800
+ * is written and imported in global mode alone.
68801
+ * @see https://docs.qoder.com/en/cli/permissions
68802
+ */
68803
+ const QODER_AUTO_MODE_KEY = "autoMode";
68804
+ /** The `autoMode` keys rulesync writes and imports. */
68805
+ const QODER_AUTO_MODE_KEYS = [
68806
+ "allow",
68807
+ "soft_deny",
68808
+ "environment"
68809
+ ];
68810
+ /** Top-level globs that keep project/local memory files from loading. */
68811
+ const QODER_AGENTS_MD_EXCLUDES_KEY = "agentsMdExcludes";
68812
+ /**
68813
+ * `general.defaultPermissionMode` values (lower-cased; Qoder matches them
68814
+ * case-insensitively) that let actions run without a prompt.
68815
+ * @see https://docs.qoder.com/en/cli/permissions
68816
+ */
68817
+ const QODER_PERMISSIVE_MODES = /* @__PURE__ */ new Set([
68818
+ "accept_edits",
68819
+ "acceptedits",
68820
+ "auto",
68821
+ "bypass_permissions",
68822
+ "bypasspermissions",
68823
+ "yolo"
68824
+ ]);
68825
+ const CATCH_ALL_PATTERN$3 = "*";
67338
68826
  /** Prefix of the fully qualified MCP tool names (`mcp__<server>__<tool>`). */
67339
- const MCP_TOOL_PREFIX = "mcp__";
68827
+ const MCP_TOOL_PREFIX$1 = "mcp__";
67340
68828
  const CATEGORY_TO_QODER_TOOL = {
67341
68829
  bash: "Bash",
67342
68830
  read: "Read",
@@ -67366,12 +68854,12 @@ const ACTIONS = [
67366
68854
  "ask",
67367
68855
  "deny"
67368
68856
  ];
67369
- const ACTION_RANK = {
68857
+ const ACTION_RANK$1 = {
67370
68858
  allow: 0,
67371
68859
  ask: 1,
67372
68860
  deny: 2
67373
68861
  };
67374
- function toStringList(value) {
68862
+ function toStringList$1(value) {
67375
68863
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
67376
68864
  }
67377
68865
  /**
@@ -67393,7 +68881,7 @@ function splitQoderRule(rule) {
67393
68881
  function categoryOfRule(rule) {
67394
68882
  const { tool } = splitQoderRule(rule);
67395
68883
  if (tool === null) return;
67396
- if (tool === "*" || tool.startsWith(MCP_TOOL_PREFIX)) return tool;
68884
+ if (tool === "*" || tool.startsWith(MCP_TOOL_PREFIX$1)) return tool;
67397
68885
  return Object.hasOwn(QODER_TOOL_TO_CATEGORY, tool) ? QODER_TOOL_TO_CATEGORY[tool] : void 0;
67398
68886
  }
67399
68887
  /**
@@ -67412,18 +68900,18 @@ function categoryOfRule(rule) {
67412
68900
  */
67413
68901
  function toQoderRule({ category, pattern, logger }) {
67414
68902
  if (category === "*") {
67415
- if (pattern === CATCH_ALL_PATTERN$2) return CATCH_ALL_PATTERN$2;
68903
+ if (pattern === CATCH_ALL_PATTERN$3) return CATCH_ALL_PATTERN$3;
67416
68904
  logger?.warn(`Qoder permissions: skipping the all-tools '*' rule '${pattern}' — a Qoder rule with a payload names one tool. Write it under the categories it is meant for (for example 'bash' or 'read').`);
67417
68905
  return null;
67418
68906
  }
67419
- if (category.startsWith(MCP_TOOL_PREFIX)) {
67420
- if (pattern === CATCH_ALL_PATTERN$2) return category;
68907
+ if (category.startsWith(MCP_TOOL_PREFIX$1)) {
68908
+ if (pattern === CATCH_ALL_PATTERN$3) return category;
67421
68909
  logger?.warn(`Qoder permissions: skipping the '${category}' rule '${pattern}' — Qoder matches MCP tools by name only.`);
67422
68910
  return null;
67423
68911
  }
67424
68912
  const tool = CATEGORY_TO_QODER_TOOL[category];
67425
68913
  if (tool === void 0) return null;
67426
- if (pattern === CATCH_ALL_PATTERN$2) return tool;
68914
+ if (pattern === CATCH_ALL_PATTERN$3) return tool;
67427
68915
  return `${QODER_PATH_RULE_ALIASES[tool] ?? tool}(${pattern})`;
67428
68916
  }
67429
68917
  /** Read a Qoder rule back into a canonical category and pattern. */
@@ -67433,9 +68921,9 @@ function fromQoderRule(rule) {
67433
68921
  const { payload } = splitQoderRule(rule);
67434
68922
  if (payload === void 0) return {
67435
68923
  category,
67436
- pattern: CATCH_ALL_PATTERN$2
68924
+ pattern: CATCH_ALL_PATTERN$3
67437
68925
  };
67438
- if (category === "*" || category.startsWith(MCP_TOOL_PREFIX)) return null;
68926
+ if (category === "*" || category.startsWith(MCP_TOOL_PREFIX$1)) return null;
67439
68927
  const trimmed = payload.trim();
67440
68928
  if (trimmed === "") return null;
67441
68929
  return {
@@ -67455,7 +68943,7 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67455
68943
  const ranked = /* @__PURE__ */ new Map();
67456
68944
  const managedCategories = /* @__PURE__ */ new Set();
67457
68945
  for (const [category, rules] of Object.entries(permission)) {
67458
- if (category !== "*" && !category.startsWith(MCP_TOOL_PREFIX) && !Object.hasOwn(CATEGORY_TO_QODER_TOOL, category)) {
68946
+ if (category !== "*" && !category.startsWith(MCP_TOOL_PREFIX$1) && !Object.hasOwn(CATEGORY_TO_QODER_TOOL, category)) {
67459
68947
  logger?.warn(`Qoder permissions: skipping the '${category}' category — Qoder has no permission rule for it.`);
67460
68948
  continue;
67461
68949
  }
@@ -67469,11 +68957,11 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67469
68957
  });
67470
68958
  if (rule === null) continue;
67471
68959
  const existing = ranked.get(rule);
67472
- if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(rule, action);
68960
+ if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) ranked.set(rule, action);
67473
68961
  }
67474
68962
  }
67475
68963
  const lists = {};
67476
- for (const action of ACTIONS) lists[action] = toStringList(existingPermissions[action]).filter((rule) => {
68964
+ for (const action of ACTIONS) lists[action] = toStringList$1(existingPermissions[action]).filter((rule) => {
67477
68965
  if (ranked.has(rule)) return false;
67478
68966
  const category = categoryOfRule(rule);
67479
68967
  return category === void 0 || !managedCategories.has(category);
@@ -67483,6 +68971,130 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67483
68971
  return lists;
67484
68972
  }
67485
68973
  /**
68974
+ * The keys of one authored override group that rulesync writes; any other key
68975
+ * is skipped with a warning.
68976
+ */
68977
+ function pickQoderGroupKeys({ group, allowedKeys, authored, where, logger }) {
68978
+ const picked = {};
68979
+ for (const [key, value] of Object.entries(authored)) {
68980
+ if (isPrototypePollutionKey(key) || value === void 0) continue;
68981
+ if (!allowedKeys.includes(key)) {
68982
+ logger?.warn(`Qoder permissions: skipping qoder.${group}.${key} — Rulesync writes only ${allowedKeys.map((k) => `${group}.${k}`).join(", ")} from the qoder override; set other keys in ${where} by hand.`);
68983
+ continue;
68984
+ }
68985
+ picked[key] = value;
68986
+ }
68987
+ return picked;
68988
+ }
68989
+ /** Name every written override setting that lets Qoder act with fewer prompts. */
68990
+ function announcePermissiveQoderSettings({ patch, where, logger }) {
68991
+ const mode = (isRecord$1(patch.general) ? patch.general : {}).defaultPermissionMode;
68992
+ if (typeof mode === "string" && QODER_PERMISSIVE_MODES.has(mode.trim().toLowerCase())) logger?.warn(`Qoder permissions: qoder.general.defaultPermissionMode = ${JSON.stringify(mode)} was written to ${where}, so Qoder starts every session it applies to in a mode that runs actions without asking first.`);
68993
+ const mcp = isRecord$1(patch.mcp) ? patch.mcp : {};
68994
+ if (mcp.enableAllProjectMcpServers === true) logger?.warn(`Qoder permissions: qoder.mcp.enableAllProjectMcpServers = true was written to ${where}, so Qoder starts every project-level MCP server without asking for approval.`);
68995
+ const enabledServers = toStringList$1(mcp.enabledProjectMcpServers);
68996
+ if (enabledServers.length > 0) logger?.warn(`Qoder permissions: qoder.mcp.enabledProjectMcpServers was written to ${where}, so Qoder starts the project-level MCP servers ${enabledServers.map((name) => JSON.stringify(name)).join(", ")} without asking for approval.`);
68997
+ if (toStringList$1((isRecord$1(patch[QODER_AUTO_MODE_KEY]) ? patch[QODER_AUTO_MODE_KEY] : {}).allow).length > 0) logger?.warn(`Qoder permissions: qoder.autoMode.allow was written to ${where}, so Qoder's Auto-mode classifier leans toward approving the actions it describes.`);
68998
+ }
68999
+ /**
69000
+ * The top-level keys the `qoder` override contributes to the settings patch,
69001
+ * besides `permissions` and `outputStyle`. Each group is the existing group
69002
+ * with the authored keys laid over it; a key is only in the patch when
69003
+ * authored. Every write that lets Qoder act with fewer prompts is announced.
69004
+ */
69005
+ function buildQoderOverridePatch({ override, existing, global, filePath, logger }) {
69006
+ const patch = {};
69007
+ if (override === void 0) return patch;
69008
+ const where = `${filePath}${global ? " in the home directory" : ""}`;
69009
+ for (const group of QODER_OVERRIDE_GROUPS) {
69010
+ const authored = override[group];
69011
+ if (!isRecord$1(authored)) continue;
69012
+ const picked = pickQoderGroupKeys({
69013
+ group,
69014
+ allowedKeys: QODER_OVERRIDE_GROUP_KEYS[group],
69015
+ authored,
69016
+ where,
69017
+ logger
69018
+ });
69019
+ if (Object.keys(picked).length > 0) patch[group] = {
69020
+ ...isRecord$1(existing[group]) ? existing[group] : {},
69021
+ ...picked
69022
+ };
69023
+ }
69024
+ const autoMode = override[QODER_AUTO_MODE_KEY];
69025
+ if (isRecord$1(autoMode)) {
69026
+ if (global) {
69027
+ const picked = pickQoderGroupKeys({
69028
+ group: QODER_AUTO_MODE_KEY,
69029
+ allowedKeys: QODER_AUTO_MODE_KEYS,
69030
+ authored: autoMode,
69031
+ where,
69032
+ logger
69033
+ });
69034
+ if (Object.keys(picked).length > 0) {
69035
+ const existingAutoMode = isRecord$1(existing[QODER_AUTO_MODE_KEY]) ? existing[QODER_AUTO_MODE_KEY] : {};
69036
+ patch[QODER_AUTO_MODE_KEY] = {
69037
+ ...existingAutoMode,
69038
+ ...picked
69039
+ };
69040
+ }
69041
+ } else logger?.warn(`Qoder permissions: skipping qoder.autoMode in project mode — Qoder reads autoMode from the user settings only. Run 'rulesync generate --global' to write it to ~/.qoder/settings.json.`);
69042
+ }
69043
+ const excludes = override[QODER_AGENTS_MD_EXCLUDES_KEY];
69044
+ if (Array.isArray(excludes)) patch[QODER_AGENTS_MD_EXCLUDES_KEY] = toStringList$1(excludes);
69045
+ announcePermissiveQoderSettings({
69046
+ patch: {
69047
+ general: override.general,
69048
+ mcp: override.mcp,
69049
+ ...global && { [QODER_AUTO_MODE_KEY]: autoMode }
69050
+ },
69051
+ where,
69052
+ logger
69053
+ });
69054
+ return patch;
69055
+ }
69056
+ /**
69057
+ * Whether a settings value has the shape the `qoder` override schema accepts
69058
+ * for that key, so an off-spec value is left in the file instead of making the
69059
+ * imported permissions file fail validation.
69060
+ */
69061
+ function isLiftableQoderValue(key, value) {
69062
+ switch (key) {
69063
+ case "defaultPermissionMode": return typeof value === "string" && value.trim() !== "";
69064
+ case "enableAllProjectMcpServers":
69065
+ case "lazyLoad": return typeof value === "boolean";
69066
+ case "overrides": return isRecord$1(value) && Object.values(value).every(isRecord$1);
69067
+ default: return Array.isArray(value) && value.every((entry) => typeof entry === "string");
69068
+ }
69069
+ }
69070
+ /**
69071
+ * Lift the settings the `qoder` override authors (other than `outputStyle`)
69072
+ * back out of a settings file. `autoMode` is lifted only from the user file,
69073
+ * the one Qoder reads it from.
69074
+ */
69075
+ function liftQoderOverride({ settings, global }) {
69076
+ const lifted = {};
69077
+ for (const group of QODER_OVERRIDE_GROUPS) {
69078
+ const existingGroup = settings[group];
69079
+ if (!isRecord$1(existingGroup)) continue;
69080
+ const picked = {};
69081
+ for (const key of QODER_OVERRIDE_GROUP_KEYS[group]) {
69082
+ const value = Object.hasOwn(existingGroup, key) ? existingGroup[key] : void 0;
69083
+ if (isLiftableQoderValue(key, value)) picked[key] = value;
69084
+ }
69085
+ if (Object.keys(picked).length > 0) lifted[group] = picked;
69086
+ }
69087
+ const autoMode = settings[QODER_AUTO_MODE_KEY];
69088
+ if (global && isRecord$1(autoMode)) {
69089
+ const picked = {};
69090
+ for (const key of QODER_AUTO_MODE_KEYS) if (Object.hasOwn(autoMode, key) && isLiftableQoderValue(key, autoMode[key])) picked[key] = autoMode[key];
69091
+ if (Object.keys(picked).length > 0) lifted[QODER_AUTO_MODE_KEY] = picked;
69092
+ }
69093
+ const excludes = settings[QODER_AGENTS_MD_EXCLUDES_KEY];
69094
+ if (isLiftableQoderValue(QODER_AGENTS_MD_EXCLUDES_KEY, excludes)) lifted[QODER_AGENTS_MD_EXCLUDES_KEY] = excludes;
69095
+ return lifted;
69096
+ }
69097
+ /**
67486
69098
  * Qoder permissions.
67487
69099
  *
67488
69100
  * Rules live under the `permissions` key of `.qoder/settings.json` (project)
@@ -67494,7 +69106,9 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67494
69106
  * (`additionalDirectories`, `trustDirectories`) are kept, and the file is
67495
69107
  * never deleted. `.qoder/settings.local.json` (where `/allow` and `/deny`
67496
69108
  * persist) is left to the user. The `qoder` override's `outputStyle` is
67497
- * written to (and imported from) the top-level `outputStyle` key.
69109
+ * written to (and imported from) the top-level `outputStyle` key, and its
69110
+ * other settings (`general`, `tools`, `mcp`, `agents`, `autoMode`,
69111
+ * `agentsMdExcludes`) through `buildQoderOverridePatch` / `liftQoderOverride`.
67498
69112
  *
67499
69113
  * @see https://docs.qoder.com/en/cli/permissions
67500
69114
  * @see https://docs.qoder.com/cli/output-styles
@@ -67557,7 +69171,14 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
67557
69171
  existingContent,
67558
69172
  patch: {
67559
69173
  [QODER_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
67560
- ...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle }
69174
+ ...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle },
69175
+ ...buildQoderOverridePatch({
69176
+ override: config.qoder,
69177
+ existing,
69178
+ global,
69179
+ filePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath),
69180
+ logger
69181
+ })
67561
69182
  },
67562
69183
  filePath
67563
69184
  }),
@@ -67572,18 +69193,24 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
67572
69193
  });
67573
69194
  const permissions = isRecord$1(settings[QODER_PERMISSIONS_KEY]) ? settings[QODER_PERMISSIONS_KEY] : {};
67574
69195
  const permission = {};
67575
- for (const action of ACTIONS) for (const rule of toStringList(permissions[action])) {
69196
+ for (const action of ACTIONS) for (const rule of toStringList$1(permissions[action])) {
67576
69197
  const parsed = fromQoderRule(rule);
67577
69198
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
67578
69199
  const rules = permission[parsed.category] ??= {};
67579
69200
  const existing = rules[parsed.pattern];
67580
- if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[parsed.pattern] = action;
69201
+ if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) rules[parsed.pattern] = action;
67581
69202
  }
67582
69203
  const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
67583
- const hasOutputStyle = typeof outputStyle === "string" && outputStyle.trim() !== "";
69204
+ const qoder = {
69205
+ ...typeof outputStyle === "string" && outputStyle.trim() !== "" && { outputStyle },
69206
+ ...liftQoderOverride({
69207
+ settings,
69208
+ global: this.global
69209
+ })
69210
+ };
67584
69211
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
67585
69212
  permission,
67586
- ...hasOutputStyle && { qoder: { outputStyle } }
69213
+ ...Object.keys(qoder).length > 0 && { qoder }
67587
69214
  }, null, 2) });
67588
69215
  }
67589
69216
  validate() {
@@ -68444,9 +70071,53 @@ const REASONIX_TRUST_AFFECTING_SANDBOX_PATHS = [
68444
70071
  path: ["workspace_root"],
68445
70072
  reason: "moves the root the file-writing tools and sandboxed Bash are confined to, so what they may reach is decided by this path rather than by the project directory",
68446
70073
  widens: escapesTheProject
70074
+ },
70075
+ {
70076
+ path: ["allow_devices"],
70077
+ reason: "binds host device nodes into the Linux Bash sandbox, which otherwise sees a private /dev",
70078
+ widens: isNonEmptyList
70079
+ },
70080
+ {
70081
+ path: ["egress_unlisted"],
70082
+ reason: "lets sandboxed Bash reach hosts that allowed_domains does not name without asking first",
70083
+ widens: (value) => typeof value !== "string" || !["", "ask"].includes(value.trim().toLowerCase())
70084
+ },
70085
+ {
70086
+ path: ["host_authorities"],
70087
+ reason: "lets sandboxed Bash call host services beyond the SSH agent, such as a container daemon socket, which amounts to host-wide execution",
70088
+ widens: (value) => !Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim() !== "ssh_agent")
68447
70089
  }
68448
70090
  ];
68449
70091
  /**
70092
+ * `[sandbox]` keys Reasonix reads only from the user config. In project scope
70093
+ * `narrowSandbox` replaces each of them with the user's value (and reports a
70094
+ * project `egress_unlisted` as user-only), so a repository cannot hand itself a
70095
+ * device, a host service or unprompted egress. A project-scope generate skips
70096
+ * them with a warning, the way `applyPermissionsMode` treats `mode`.
70097
+ *
70098
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.44.0/internal/contract/config/project_scope.go
70099
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.44.0/internal/contract/config/config.go
70100
+ */
70101
+ const REASONIX_USER_ONLY_SANDBOX_KEYS = [
70102
+ "allow_devices",
70103
+ "egress_unlisted",
70104
+ "host_authorities"
70105
+ ];
70106
+ /**
70107
+ * Removes the user-only `[sandbox]` keys from a project-scope override, warning
70108
+ * once about what was skipped. The keys are left alone in the file, so whatever
70109
+ * it already had survives the shallow merge. Global scope keeps them all, and
70110
+ * a non-table override is returned as is, so the trust check still reports its
70111
+ * shape rather than seeing nothing.
70112
+ */
70113
+ function dropUserOnlySandboxKeys({ sandbox, global, relativeFilePath, logger }) {
70114
+ if (global || !isRecord$1(sandbox)) return sandbox;
70115
+ const skipped = REASONIX_USER_ONLY_SANDBOX_KEYS.filter((key) => sandbox[key] !== void 0);
70116
+ if (skipped.length === 0) return sandbox;
70117
+ logger?.warn(`Reasonix permissions: skipping ${skipped.map((key) => `reasonix.sandbox.${key}`).join(", ")} for the project-scoped ${relativeFilePath}; Reasonix reads ${skipped.length === 1 ? "this key" : "these keys"} only from the user config and replaces a project value with the user's, so generate it with --global instead.`);
70118
+ return Object.fromEntries(Object.entries(sandbox).filter(([key]) => !skipped.includes(key)));
70119
+ }
70120
+ /**
68450
70121
  * Whether a `workspace_root` points somewhere other than inside the project the
68451
70122
  * generate runs in. The key moves the write confinement rather than adding to
68452
70123
  * it, so the ordinary value — the project directory, spelled relatively — would
@@ -68683,14 +70354,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
68683
70354
  }));
68684
70355
  const patch = { permissions: mergedPermissions };
68685
70356
  if (override?.sandbox !== void 0) {
70357
+ const authoredSandbox = dropUserOnlySandboxKeys({
70358
+ sandbox: override.sandbox,
70359
+ global,
70360
+ relativeFilePath: relativeFilePathForLog,
70361
+ logger
70362
+ });
68686
70363
  const mergedSandbox = {
68687
70364
  ...asReasonixRecord(parsed.sandbox),
68688
- ...asReasonixRecord(override.sandbox)
70365
+ ...asReasonixRecord(authoredSandbox)
68689
70366
  };
68690
70367
  patch.sandbox = mergedSandbox;
68691
70368
  trustAffecting.push(...collectSandboxOverlayEntries({
68692
70369
  existing: parsed.sandbox,
68693
- authored: override.sandbox,
70370
+ authored: authoredSandbox,
68694
70371
  merged: mergedSandbox
68695
70372
  }));
68696
70373
  }
@@ -68743,7 +70420,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
68743
70420
  ask: askSplit.translated,
68744
70421
  deny: denySplit.translated
68745
70422
  });
68746
- const sandbox = asReasonixRecord(this.toml.sandbox);
70423
+ const sandbox = Object.fromEntries(Object.entries(asReasonixRecord(this.toml.sandbox)).filter(([key]) => this.global || !REASONIX_USER_ONLY_SANDBOX_KEYS.includes(key)));
68747
70424
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
68748
70425
  const reasonixOverride = {};
68749
70426
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
@@ -69049,7 +70726,7 @@ var RooPermissions = class extends ToolPermissions {
69049
70726
  };
69050
70727
  //#endregion
69051
70728
  //#region src/features/permissions/rovodev-permissions.ts
69052
- const CATCH_ALL_PATTERN$1 = "*";
70729
+ const CATCH_ALL_PATTERN$2 = "*";
69053
70730
  const CATEGORY_TO_TOOL_KEYS = {
69054
70731
  read: [
69055
70732
  "open_files",
@@ -69380,7 +71057,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
69380
71057
  logger
69381
71058
  });
69382
71059
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
69383
- if (category === CATCH_ALL_PATTERN$1) {
71060
+ if (category === CATCH_ALL_PATTERN$2) {
69384
71061
  const toolWideDefault = convertAllToolsRules({
69385
71062
  rules,
69386
71063
  logger
@@ -69399,7 +71076,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
69399
71076
  continue;
69400
71077
  }
69401
71078
  for (const [pattern, action] of Object.entries(rules)) {
69402
- if (pattern === CATCH_ALL_PATTERN$1) {
71079
+ if (pattern === CATCH_ALL_PATTERN$2) {
69403
71080
  toolPermissions.tools ??= {};
69404
71081
  for (const toolKey of toolKeys) toolPermissions.tools[toolKey] = strictestAction(toolPermissions.tools[toolKey], action);
69405
71082
  continue;
@@ -69422,8 +71099,8 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
69422
71099
  * resolution never grants more than the author asked for.
69423
71100
  */
69424
71101
  function warnOnEditWriteConflict({ config, logger }) {
69425
- const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
69426
- const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
71102
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$2];
71103
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$2];
69427
71104
  if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
69428
71105
  }
69429
71106
  /**
@@ -69436,7 +71113,7 @@ function warnOnEditWriteConflict({ config, logger }) {
69436
71113
  function convertAllToolsRules({ rules, logger }) {
69437
71114
  let toolWideDefault;
69438
71115
  for (const [pattern, action] of Object.entries(rules)) {
69439
- if (pattern === CATCH_ALL_PATTERN$1) {
71116
+ if (pattern === CATCH_ALL_PATTERN$2) {
69440
71117
  toolWideDefault = action;
69441
71118
  continue;
69442
71119
  }
@@ -69448,7 +71125,7 @@ function convertBashRules(rules) {
69448
71125
  const bash = {};
69449
71126
  const commands = [];
69450
71127
  for (const [pattern, action] of Object.entries(rules)) {
69451
- if (pattern === CATCH_ALL_PATTERN$1) {
71128
+ if (pattern === CATCH_ALL_PATTERN$2) {
69452
71129
  bash.default = action;
69453
71130
  continue;
69454
71131
  }
@@ -69465,26 +71142,26 @@ function convertBashRules(rules) {
69465
71142
  */
69466
71143
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
69467
71144
  const permission = {};
69468
- if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
71145
+ if (isPermissionAction$1(toolPermissions.default)) permission[CATCH_ALL_PATTERN$2] = { [CATCH_ALL_PATTERN$2]: toolPermissions.default };
69469
71146
  const bash = toolPermissions.bash;
69470
71147
  if (isRecord$1(bash)) {
69471
71148
  const bashRules = {};
69472
- if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
71149
+ if (isPermissionAction$1(bash.default)) bashRules[CATCH_ALL_PATTERN$2] = bash.default;
69473
71150
  if (Array.isArray(bash.commands)) {
69474
- for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
71151
+ for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction$1(entry.permission)) bashRules[entry.command] = entry.permission;
69475
71152
  }
69476
71153
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
69477
71154
  }
69478
71155
  const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
69479
- const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
71156
+ const implicitLevel = isPermissionAction$1(toolPermissions.default) ? toolPermissions.default : "ask";
69480
71157
  for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
69481
71158
  const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
69482
71159
  const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
69483
- return isPermissionAction(value) ? value : void 0;
71160
+ return isPermissionAction$1(value) ? value : void 0;
69484
71161
  });
69485
71162
  if (levels.every((level) => level === void 0)) continue;
69486
71163
  permission[category] ??= {};
69487
- permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
71164
+ permission[category][CATCH_ALL_PATTERN$2] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$2]);
69488
71165
  }
69489
71166
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
69490
71167
  permission.read ??= {};
@@ -69501,7 +71178,7 @@ function strictestAction(current, candidate) {
69501
71178
  if (current === void 0) return candidate;
69502
71179
  return ACTION_STRICTNESS.indexOf(current) <= ACTION_STRICTNESS.indexOf(candidate) ? current : candidate;
69503
71180
  }
69504
- function isPermissionAction(value) {
71181
+ function isPermissionAction$1(value) {
69505
71182
  return PermissionActionSchema.safeParse(value).success;
69506
71183
  }
69507
71184
  //#endregion
@@ -70208,7 +71885,7 @@ const TAKT_SECURITY_POLICIES = {
70208
71885
  const TAKT_SECURITY_POLICY_KEYS = Object.keys(TAKT_SECURITY_POLICIES);
70209
71886
  const TAKT_DEFAULT_PROVIDER = "claude-sdk";
70210
71887
  const TAKT_LEGACY_DEFAULT_PROVIDER = "claude";
70211
- const CATCH_ALL_PATTERN = "*";
71888
+ const CATCH_ALL_PATTERN$1 = "*";
70212
71889
  /**
70213
71890
  * Permissions adapter for Takt (`.takt/config.yaml`).
70214
71891
  *
@@ -70829,9 +72506,9 @@ function deriveTaktPermissionMode(config) {
70829
72506
  */
70830
72507
  function taktModeToRulesyncConfig(mode) {
70831
72508
  switch (mode) {
70832
- case "full": return { permission: { bash: { [CATCH_ALL_PATTERN]: "allow" } } };
70833
- case "edit": return { permission: { edit: { [CATCH_ALL_PATTERN]: "allow" } } };
70834
- default: return { permission: { bash: { [CATCH_ALL_PATTERN]: "deny" } } };
72509
+ case "full": return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "allow" } } };
72510
+ case "edit": return { permission: { edit: { [CATCH_ALL_PATTERN$1]: "allow" } } };
72511
+ default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
70835
72512
  }
70836
72513
  }
70837
72514
  /**
@@ -70864,6 +72541,409 @@ function pickSecurityPolicies(source, report) {
70864
72541
  return picked;
70865
72542
  }
70866
72543
  //#endregion
72544
+ //#region src/features/permissions/trae-permissions.ts
72545
+ const CUSTOM_PROFILES_KEY = "customProfiles";
72546
+ const DEFAULT_CUSTOM_PROFILE_KEY = "defaultCustomProfile";
72547
+ const APPROVAL_KEY = "approval";
72548
+ const COMMAND_RULES_KEY = "commandRules";
72549
+ const MCP_RULES_KEY = "mcpRules";
72550
+ const RESOURCE_AUTHORIZATION_KEY = "resourceAuthorization";
72551
+ const NETWORK_KEY = "network";
72552
+ const BASH_CATEGORY = "bash";
72553
+ const WEBFETCH_CATEGORY = "webfetch";
72554
+ const MCP_TOOL_PREFIX = "mcp__";
72555
+ const MCP_SEPARATOR = "__";
72556
+ const CATCH_ALL_PATTERN = "*";
72557
+ const REGEX_PREFIX = "r/";
72558
+ const REGEX_SUFFIX = "/";
72559
+ const ACTION_RANK = {
72560
+ allow: 0,
72561
+ ask: 1,
72562
+ deny: 2
72563
+ };
72564
+ const NETWORK_ACTIONS = ["allow", "deny"];
72565
+ function isPermissionAction(value) {
72566
+ return value === "allow" || value === "ask" || value === "deny";
72567
+ }
72568
+ function toStringList(value) {
72569
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
72570
+ }
72571
+ function escapeRegex(text) {
72572
+ return text.replace(/[\\^$.|?*+()[\]{}/]/g, "\\$&");
72573
+ }
72574
+ /** Build the anchored TRAE regex rule (`r/^...$/`) a canonical glob stands for. */
72575
+ function globToTraeRegex(pattern) {
72576
+ const body = pattern.split(CATCH_ALL_PATTERN).map(escapeRegex).join(".*");
72577
+ return `${REGEX_PREFIX}^${body}$${REGEX_SUFFIX}`;
72578
+ }
72579
+ /**
72580
+ * Translate a canonical `bash` glob into a TRAE `commandRules` key. TRAE
72581
+ * matches a key exactly (`git status`), as a prefix when it ends in `*`
72582
+ * (`git add *`), or as a regex when it is written `r/.../`. A glob whose only
72583
+ * `*` is the trailing one is already a TRAE prefix rule and a glob without
72584
+ * `*` an exact one; every other glob (a bare `*`, an inner `*`, or a literal
72585
+ * that would read as `r/.../`) becomes an anchored regex.
72586
+ */
72587
+ function toTraeCommandKey(pattern) {
72588
+ const starCount = pattern.split(CATCH_ALL_PATTERN).length - 1;
72589
+ const looksLikeRegex = pattern.startsWith(REGEX_PREFIX) && pattern.endsWith(REGEX_SUFFIX);
72590
+ if (!looksLikeRegex && starCount === 0) return pattern;
72591
+ if (!looksLikeRegex && starCount === 1 && pattern.length > 1 && pattern.endsWith("*")) return pattern;
72592
+ return globToTraeRegex(pattern);
72593
+ }
72594
+ /**
72595
+ * Read a `commandRules` key back into a canonical `bash` glob, or `null` for a
72596
+ * key with no glob spelling: a regex rulesync did not write, or an exact key
72597
+ * with an inner `*` (TRAE matches that `*` literally).
72598
+ */
72599
+ function fromTraeCommandKey(key) {
72600
+ if (!(key.startsWith(REGEX_PREFIX) && key.endsWith(REGEX_SUFFIX) && key.length > 2)) return toTraeCommandKey(key) === key ? key : null;
72601
+ const anchored = key.slice(2, -1).match(/^\^([\s\S]*)\$$/);
72602
+ if (!anchored) return null;
72603
+ const glob = (anchored[1] ?? "").split(".*").map((part) => part.replace(/\\(.)/g, "$1")).join(CATCH_ALL_PATTERN);
72604
+ return globToTraeRegex(glob) === key ? glob : null;
72605
+ }
72606
+ /**
72607
+ * Translate a canonical MCP category (`mcp__<server>__<tool>`,
72608
+ * `mcp__<server>__*` or `mcp__<server>`) into a TRAE `mcpRules` key
72609
+ * (`<server>__<tool>`, `<server>__*`, `<server>`). `null` when TRAE has no
72610
+ * spelling for it (`mcp__*`, which names no server).
72611
+ */
72612
+ function toTraeMcpKey(category) {
72613
+ const rest = category.slice(5);
72614
+ const server = rest.split(MCP_SEPARATOR)[0] ?? "";
72615
+ if (server === "" || server.includes(CATCH_ALL_PATTERN)) return null;
72616
+ return rest;
72617
+ }
72618
+ /** Read a TRAE `mcpRules` key back into its canonical MCP category. */
72619
+ function fromTraeMcpKey(key) {
72620
+ if (key === "" || key.startsWith(MCP_SEPARATOR)) return null;
72621
+ return key.includes(MCP_SEPARATOR) ? `${MCP_TOOL_PREFIX}${key}` : `${MCP_TOOL_PREFIX}${key}${MCP_SEPARATOR}${CATCH_ALL_PATTERN}`;
72622
+ }
72623
+ function readRecord(source, path) {
72624
+ let current = source;
72625
+ for (const key of path) {
72626
+ if (!isRecord$1(current)) return {};
72627
+ current = current[key];
72628
+ }
72629
+ return isRecord$1(current) ? current : {};
72630
+ }
72631
+ /**
72632
+ * Return a copy of `root` with `value` at `path` (or the leaf removed when
72633
+ * `value` is `undefined`), creating the parent objects only when setting.
72634
+ */
72635
+ function withPath(root, path, value) {
72636
+ const [head, ...rest] = path;
72637
+ if (head === void 0) return root;
72638
+ const next = { ...root };
72639
+ if (rest.length === 0) {
72640
+ if (value === void 0) delete next[head];
72641
+ else next[head] = value;
72642
+ return next;
72643
+ }
72644
+ const child = next[head];
72645
+ if (!isRecord$1(child)) {
72646
+ if (value === void 0) return next;
72647
+ next[head] = withPath({}, rest, value);
72648
+ return next;
72649
+ }
72650
+ next[head] = withPath(child, rest, value);
72651
+ return next;
72652
+ }
72653
+ const COMMAND_RULES_PATH = [
72654
+ DEFAULT_CUSTOM_PROFILE_KEY,
72655
+ APPROVAL_KEY,
72656
+ COMMAND_RULES_KEY
72657
+ ];
72658
+ const MCP_RULES_PATH = [
72659
+ DEFAULT_CUSTOM_PROFILE_KEY,
72660
+ APPROVAL_KEY,
72661
+ MCP_RULES_KEY
72662
+ ];
72663
+ function setRanked(ranked, key, action) {
72664
+ const existing = ranked.get(key);
72665
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(key, action);
72666
+ }
72667
+ /**
72668
+ * Rebuild `commandRules` from the canonical `bash` category. An existing
72669
+ * entry rulesync cannot read back (a hand-written regex) is kept, and the
72670
+ * `execEnv` of an entry this run rewrites is carried over.
72671
+ */
72672
+ function buildCommandRules({ bashRules, existing }) {
72673
+ const ranked = /* @__PURE__ */ new Map();
72674
+ for (const [pattern, action] of Object.entries(bashRules)) {
72675
+ if (isPrototypePollutionKey(pattern)) continue;
72676
+ setRanked(ranked, toTraeCommandKey(pattern), action);
72677
+ }
72678
+ const result = {};
72679
+ for (const [key, value] of Object.entries(existing)) if (!ranked.has(key) && fromTraeCommandKey(key) === null) result[key] = value;
72680
+ for (const [key, action] of ranked) {
72681
+ const previous = existing[key];
72682
+ const execEnv = isRecord$1(previous) ? previous.execEnv : void 0;
72683
+ result[key] = {
72684
+ approval: action,
72685
+ ...typeof execEnv === "string" && { execEnv }
72686
+ };
72687
+ }
72688
+ return result;
72689
+ }
72690
+ /**
72691
+ * Rebuild `mcpRules`: every MCP category of the canonical config is written,
72692
+ * and an existing entry whose category the config does not name is kept.
72693
+ */
72694
+ function buildMcpRules({ permission, existing, logger }) {
72695
+ const ranked = /* @__PURE__ */ new Map();
72696
+ const managedCategories = /* @__PURE__ */ new Set();
72697
+ for (const [category, rules] of Object.entries(permission)) {
72698
+ if (!category.startsWith(MCP_TOOL_PREFIX)) continue;
72699
+ const key = toTraeMcpKey(category);
72700
+ if (key === null) {
72701
+ logger?.warn(`TRAE permissions: skipping the '${category}' category — TRAE's mcpRules name an MCP server ('server', 'server__*' or 'server__tool').`);
72702
+ continue;
72703
+ }
72704
+ managedCategories.add(category);
72705
+ for (const [pattern, action] of Object.entries(rules)) {
72706
+ if (pattern !== CATCH_ALL_PATTERN) {
72707
+ logger?.warn(`TRAE permissions: skipping the '${category}' rule '${pattern}' — TRAE matches MCP tools by name only.`);
72708
+ continue;
72709
+ }
72710
+ setRanked(ranked, key, action);
72711
+ }
72712
+ }
72713
+ const result = {};
72714
+ for (const [key, value] of Object.entries(existing)) {
72715
+ const category = fromTraeMcpKey(key);
72716
+ if (!ranked.has(key) && (category === null || !managedCategories.has(category))) result[key] = value;
72717
+ }
72718
+ for (const [key, action] of ranked) result[key] = { approval: action };
72719
+ return result;
72720
+ }
72721
+ /** Split the canonical `webfetch` rules into TRAE's network allow/deny lists. */
72722
+ function buildNetworkLists({ webfetchRules, logger }) {
72723
+ const ranked = /* @__PURE__ */ new Map();
72724
+ for (const [pattern, action] of Object.entries(webfetchRules)) {
72725
+ if (isPrototypePollutionKey(pattern)) continue;
72726
+ if (pattern === CATCH_ALL_PATTERN) {
72727
+ logger?.warn(`TRAE permissions: skipping the 'webfetch' rule '*' — TRAE sets the default network policy in the custom permission mode ('customProfiles.defaultCustomProfile.network.default'), not in resourceAuthorization.`);
72728
+ continue;
72729
+ }
72730
+ if (action === "ask") {
72731
+ logger?.warn(`TRAE permissions: skipping the 'webfetch' ask rule '${pattern}' — resourceAuthorization.network has only allow and deny lists.`);
72732
+ continue;
72733
+ }
72734
+ setRanked(ranked, pattern, action);
72735
+ }
72736
+ const lists = {
72737
+ allow: [],
72738
+ deny: []
72739
+ };
72740
+ for (const [pattern, action] of ranked) if (action === "allow" || action === "deny") lists[action].push(pattern);
72741
+ return lists;
72742
+ }
72743
+ function warnUnmappedCategories({ permission, logger }) {
72744
+ for (const category of Object.keys(permission)) if (category !== BASH_CATEGORY && category !== WEBFETCH_CATEGORY && !category.startsWith(MCP_TOOL_PREFIX)) logger?.warn(`TRAE permissions: skipping the '${category}' category — TRAE's global permission file has no rule for it (only 'bash', 'webfetch' and 'mcp__*' are mapped).`);
72745
+ }
72746
+ /** A rebuilt map, or `undefined` (remove the key) when it ended up empty. */
72747
+ function nonEmpty(value) {
72748
+ return Object.keys(value).length > 0 ? value : void 0;
72749
+ }
72750
+ /**
72751
+ * The new `customProfiles` value: the existing one with `commandRules` and
72752
+ * `mcpRules` rebuilt for the categories the config names. The existing value
72753
+ * is returned untouched when the config names none of them.
72754
+ */
72755
+ function buildCustomProfiles({ existing, permission, logger }) {
72756
+ const bashRules = permission[BASH_CATEGORY];
72757
+ const managesMcp = Object.keys(permission).some((category) => category.startsWith(MCP_TOOL_PREFIX));
72758
+ if (bashRules === void 0 && !managesMcp) return existing;
72759
+ let result = isRecord$1(existing) ? existing : {};
72760
+ if (bashRules !== void 0) {
72761
+ const commandRules = buildCommandRules({
72762
+ bashRules,
72763
+ existing: readRecord(result, COMMAND_RULES_PATH)
72764
+ });
72765
+ result = withPath(result, COMMAND_RULES_PATH, nonEmpty(commandRules));
72766
+ }
72767
+ if (managesMcp) {
72768
+ const mcpRules = buildMcpRules({
72769
+ permission,
72770
+ existing: readRecord(result, MCP_RULES_PATH),
72771
+ logger
72772
+ });
72773
+ result = withPath(result, MCP_RULES_PATH, nonEmpty(mcpRules));
72774
+ }
72775
+ return existing === void 0 ? nonEmpty(result) : result;
72776
+ }
72777
+ /**
72778
+ * The new `resourceAuthorization` value: the existing one with
72779
+ * `network.allow` / `deny` rebuilt from the `webfetch` category, or the
72780
+ * existing value untouched when the config has no `webfetch` category.
72781
+ */
72782
+ function buildResourceAuthorization({ existing, permission, logger }) {
72783
+ const webfetchRules = permission[WEBFETCH_CATEGORY];
72784
+ if (webfetchRules === void 0) return existing;
72785
+ const lists = buildNetworkLists({
72786
+ webfetchRules,
72787
+ logger
72788
+ });
72789
+ let result = isRecord$1(existing) ? existing : {};
72790
+ for (const action of NETWORK_ACTIONS) result = withPath(result, [NETWORK_KEY, action], nonEmpty(lists[action]));
72791
+ return existing === void 0 ? nonEmpty(result) : result;
72792
+ }
72793
+ /**
72794
+ * TRAE permissions (global scope only).
72795
+ *
72796
+ * TRAE keeps its global permission policy and its custom permission mode in
72797
+ * one user file, `~/.trae/permission/global.json`. Rulesync writes three parts
72798
+ * of it and leaves the rest (`shellSandbox`, `approval.reviewer`,
72799
+ * `sceneRules`, the default `filesystem` / `network` policy,
72800
+ * `resourceAuthorization.filesystem`) as it is:
72801
+ *
72802
+ * - `bash` → `customProfiles.defaultCustomProfile.approval.commandRules`,
72803
+ * `{ "<pattern>": { "approval": "allow" | "ask" | "deny" } }`. A pattern is
72804
+ * matched exactly, as a prefix when it ends in `*`, or as a regex written
72805
+ * `r/.../`; other globs become anchored regexes.
72806
+ * - `mcp__<server>__<tool>` / `mcp__<server>__*` →
72807
+ * `customProfiles.defaultCustomProfile.approval.mcpRules`
72808
+ * (`<server>__<tool>` / `<server>__*`).
72809
+ * - `webfetch` domains → `resourceAuthorization.network.allow` / `deny`.
72810
+ *
72811
+ * The file is never deleted, and an unparseable file is an error rather than
72812
+ * being replaced.
72813
+ *
72814
+ * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
72815
+ * @see https://docs.trae.ai/ide/custom-global-permission-configuration-reference?_lang=en
72816
+ * @see https://docs.trae.ai/ide/custom-permission-mode-configuration-reference?_lang=en
72817
+ */
72818
+ var TraePermissions = class TraePermissions extends ToolPermissions {
72819
+ constructor(params) {
72820
+ super({
72821
+ ...params,
72822
+ fileContent: params.fileContent ?? "{}"
72823
+ });
72824
+ }
72825
+ isDeletable() {
72826
+ return false;
72827
+ }
72828
+ static getSettablePaths(_options = {}) {
72829
+ return {
72830
+ relativeDirPath: TRAE_PERMISSION_DIR_PATH,
72831
+ relativeFilePath: TRAE_PERMISSION_FILE_NAME
72832
+ };
72833
+ }
72834
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = true }) {
72835
+ const paths = TraePermissions.getSettablePaths({ global });
72836
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
72837
+ return new TraePermissions({
72838
+ outputRoot,
72839
+ relativeDirPath: paths.relativeDirPath,
72840
+ relativeFilePath: paths.relativeFilePath,
72841
+ fileContent,
72842
+ validate,
72843
+ global
72844
+ });
72845
+ }
72846
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = true, logger }) {
72847
+ const paths = TraePermissions.getSettablePaths({ global });
72848
+ const relativePath = (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath);
72849
+ const filePath = (0, node_path.join)(outputRoot, relativePath);
72850
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
72851
+ const existing = parseTraePermissionConfig({
72852
+ fileContent: existingContent,
72853
+ relativePath
72854
+ });
72855
+ const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
72856
+ warnUnmappedCategories({
72857
+ permission,
72858
+ logger
72859
+ });
72860
+ return new TraePermissions({
72861
+ outputRoot,
72862
+ relativeDirPath: paths.relativeDirPath,
72863
+ relativeFilePath: paths.relativeFilePath,
72864
+ fileContent: applySharedConfigPatch({
72865
+ fileKey: sharedConfigFileKey(paths),
72866
+ feature: "permissions",
72867
+ existingContent,
72868
+ patch: {
72869
+ [CUSTOM_PROFILES_KEY]: buildCustomProfiles({
72870
+ existing: existing[CUSTOM_PROFILES_KEY],
72871
+ permission,
72872
+ logger
72873
+ }),
72874
+ [RESOURCE_AUTHORIZATION_KEY]: buildResourceAuthorization({
72875
+ existing: existing[RESOURCE_AUTHORIZATION_KEY],
72876
+ permission,
72877
+ logger
72878
+ })
72879
+ },
72880
+ filePath
72881
+ }),
72882
+ validate: true,
72883
+ global
72884
+ });
72885
+ }
72886
+ toRulesyncPermissions() {
72887
+ const config = parseTraePermissionConfig({
72888
+ fileContent: this.getFileContent() || "{}",
72889
+ relativePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
72890
+ });
72891
+ const customProfiles = readRecord(config, [CUSTOM_PROFILES_KEY]);
72892
+ const permission = {};
72893
+ const put = (category, pattern, action) => {
72894
+ if (isPrototypePollutionKey(category) || isPrototypePollutionKey(pattern)) return;
72895
+ const rules = permission[category] ??= {};
72896
+ const existing = rules[pattern];
72897
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[pattern] = action;
72898
+ };
72899
+ for (const [key, value] of Object.entries(readRecord(customProfiles, COMMAND_RULES_PATH))) {
72900
+ const pattern = fromTraeCommandKey(key);
72901
+ const approval = isRecord$1(value) ? value.approval : void 0;
72902
+ if (pattern !== null && pattern.trim() !== "" && isPermissionAction(approval)) put(BASH_CATEGORY, pattern, approval);
72903
+ }
72904
+ for (const [key, value] of Object.entries(readRecord(customProfiles, MCP_RULES_PATH))) {
72905
+ const category = fromTraeMcpKey(key);
72906
+ const approval = isRecord$1(value) ? value.approval : void 0;
72907
+ if (category !== null && isPermissionAction(approval)) put(category, CATCH_ALL_PATTERN, approval);
72908
+ }
72909
+ const network = readRecord(config, [RESOURCE_AUTHORIZATION_KEY, NETWORK_KEY]);
72910
+ for (const action of NETWORK_ACTIONS) for (const pattern of toStringList(network[action])) if (pattern.trim() !== "") put(WEBFETCH_CATEGORY, pattern, action);
72911
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
72912
+ }
72913
+ validate() {
72914
+ return {
72915
+ success: true,
72916
+ error: null
72917
+ };
72918
+ }
72919
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = true }) {
72920
+ return new TraePermissions({
72921
+ outputRoot,
72922
+ relativeDirPath,
72923
+ relativeFilePath,
72924
+ fileContent: "{}",
72925
+ validate: false,
72926
+ global
72927
+ });
72928
+ }
72929
+ };
72930
+ /**
72931
+ * Fail closed on a syntax error or non-object root, so a broken file is
72932
+ * surfaced rather than partially imported or overwritten.
72933
+ */
72934
+ function parseTraePermissionConfig({ fileContent, relativePath }) {
72935
+ try {
72936
+ return parseSharedConfig({
72937
+ format: "json",
72938
+ fileContent,
72939
+ filePath: relativePath,
72940
+ invalidRootPolicy: "error"
72941
+ });
72942
+ } catch (error) {
72943
+ throw new Error(`Failed to parse TRAE permission config in ${relativePath}: ${formatError(error)}`, { cause: error });
72944
+ }
72945
+ }
72946
+ //#endregion
70867
72947
  //#region src/utils/display-width.ts
70868
72948
  /**
70869
72949
  * Characters a terminal draws two columns wide: the East Asian Wide and
@@ -74149,6 +76229,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
74149
76229
  supportsImport: true
74150
76230
  }
74151
76231
  }],
76232
+ ["trae", {
76233
+ class: TraePermissions,
76234
+ meta: {
76235
+ supportsProject: false,
76236
+ supportsGlobal: true,
76237
+ supportsImport: true
76238
+ }
76239
+ }],
74152
76240
  ["zoocode", {
74153
76241
  class: ZoocodePermissions,
74154
76242
  meta: {
@@ -74253,7 +76341,7 @@ var PermissionsProcessor = class extends FeatureProcessor {
74253
76341
  })];
74254
76342
  }
74255
76343
  async convertToolFilesToRulesyncFiles(toolFiles) {
74256
- return toolFiles.filter((f) => f instanceof ToolPermissions).map((p) => p.toRulesyncPermissions());
76344
+ return toolFiles.filter((f) => f instanceof ToolPermissions).map((p) => p.toRulesyncPermissions({ logger: this.logger }));
74257
76345
  }
74258
76346
  static getToolTargets({ global = false, importOnly = false } = {}) {
74259
76347
  return [...toolPermissionsFactories.entries()].filter(([, f]) => global ? f.meta.supportsGlobal : f.meta.supportsProject).filter(([, f]) => importOnly ? f.meta.supportsImport : true).map(([target]) => target);
@@ -79166,8 +81254,13 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
79166
81254
  * slash command), so this target reuses {@link AgentsSkillsSkill} with GitLab's
79167
81255
  * locations:
79168
81256
  *
79169
- * - Project skills live in a non-hidden `skills/<name>/SKILL.md` at the project
79170
- * root, which both the GitLab UI flows and the GitLab Duo CLI read.
81257
+ * - Project skills are written to the cross-tool `.agents/skills/<name>/SKILL.md`
81258
+ * root, which both the GitLab UI flows and the GitLab Duo CLI read. GitLab
81259
+ * Duo also reads a non-hidden `skills/<name>/SKILL.md` at the project root
81260
+ * (`.agents/skills/` wins on a name clash); rulesync imports from it but
81261
+ * never writes or sweeps it, because that directory routinely holds
81262
+ * hand-authored skills. Earlier rulesync versions wrote there, so a skill
81263
+ * generated by one of them has to be removed from `skills/` by hand.
79171
81264
  * - User skills live in `~/.gitlab/duo/skills/` and are loaded by the CLI only
79172
81265
  * when started with `--enable-global-skills true` (or
79173
81266
  * `GITLAB_ENABLE_GLOBAL_SKILLS=true`).
@@ -79176,18 +81269,20 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
79176
81269
  */
79177
81270
  var GitlabduoSkill = class extends AgentsSkillsSkill {
79178
81271
  static getSettablePaths({ global = false } = {}) {
79179
- return { relativeDirPath: global ? GITLABDUO_GLOBAL_SKILLS_DIR : GITLABDUO_SKILLS_DIR };
81272
+ if (global) return { relativeDirPath: GITLABDUO_GLOBAL_SKILLS_DIR };
81273
+ return {
81274
+ relativeDirPath: GITLABDUO_SKILLS_DIR,
81275
+ importOnlySkillRoots: [GITLABDUO_LEGACY_SKILLS_DIR]
81276
+ };
79180
81277
  }
79181
81278
  static isTargetedByRulesyncSkill(rulesyncSkill) {
79182
81279
  const targets = rulesyncSkill.getFrontmatter().targets;
79183
81280
  return targets.includes("*") || targets.includes("gitlabduo");
79184
81281
  }
79185
81282
  /**
79186
- * The project skills root is a plain `skills/` directory that many
79187
- * repositories already use for unrelated content, so only a subdirectory
79188
- * holding a `SKILL.md` is treated as a skill. (At project scope the
79189
- * `--delete` orphan sweep skips that root entirely through the factory's
79190
- * `committedOutput` meta; this filter still applies to the global sweep.)
81283
+ * The import-only project-root `skills/` directory is one many repositories
81284
+ * already use for unrelated content, so only a subdirectory holding a
81285
+ * `SKILL.md` is treated as a skill, in every root this target reads.
79191
81286
  */
79192
81287
  static async isDirOwned({ outputRoot, relativeDirPath, dirName }) {
79193
81288
  return await fileExists((0, node_path.join)(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME));
@@ -80740,9 +82835,44 @@ var MimocodeSkill = class extends OpenCodeSkill {
80740
82835
  //#region src/features/skills/musecode-skill.ts
80741
82836
  const MusecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
80742
82837
  name: zod_mini.z.string(),
80743
- description: zod_mini.z.string()
82838
+ description: zod_mini.z.string(),
82839
+ license: zod_mini.z.optional(zod_mini.z.string()),
82840
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
82841
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
80744
82842
  });
80745
82843
  /**
82844
+ * Build the Muse Code frontmatter. `license`, `compatibility` and `metadata`
82845
+ * come from the `musecode` section with the root-level values as fallback, and
82846
+ * are normalized exactly as the `agentsskills` writer normalizes them: at
82847
+ * project scope both targets write the same `.agents/skills/<name>/SKILL.md`,
82848
+ * so for a skill without per-target overrides the file no longer depends on
82849
+ * which of the two runs last.
82850
+ */
82851
+ function buildMusecodeFrontmatter(rulesyncFrontmatter) {
82852
+ const section = rulesyncFrontmatter.musecode;
82853
+ const license = resolveLicense({
82854
+ rootFrontmatter: rulesyncFrontmatter,
82855
+ section
82856
+ });
82857
+ const compatibility = resolveCompatibility({
82858
+ rootFrontmatter: rulesyncFrontmatter,
82859
+ section
82860
+ });
82861
+ const metadata = resolveMetadata({
82862
+ rootFrontmatter: rulesyncFrontmatter,
82863
+ section
82864
+ });
82865
+ return {
82866
+ name: rulesyncFrontmatter.name,
82867
+ description: rulesyncFrontmatter.description,
82868
+ ...toSpecConformantAgentSkillFields({
82869
+ ...license !== void 0 && { license },
82870
+ ...compatibility !== void 0 && { compatibility },
82871
+ ...metadata !== void 0 && { metadata }
82872
+ })
82873
+ };
82874
+ }
82875
+ /**
80746
82876
  * Represents a Meta Muse Code skill directory.
80747
82877
  *
80748
82878
  * Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
@@ -80799,10 +82929,16 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
80799
82929
  }
80800
82930
  toRulesyncSkill() {
80801
82931
  const frontmatter = this.getFrontmatter();
82932
+ const musecodeSection = {
82933
+ ...frontmatter.license !== void 0 && { license: frontmatter.license },
82934
+ ...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
82935
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
82936
+ };
80802
82937
  const rulesyncFrontmatter = {
80803
82938
  name: frontmatter.name,
80804
82939
  description: frontmatter.description,
80805
- targets: ["*"]
82940
+ targets: ["*"],
82941
+ ...Object.keys(musecodeSection).length > 0 && { musecode: musecodeSection }
80806
82942
  };
80807
82943
  return new RulesyncSkill({
80808
82944
  outputRoot: this.outputRoot,
@@ -80817,11 +82953,7 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
80817
82953
  }
80818
82954
  static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
80819
82955
  const settablePaths = MusecodeSkill.getSettablePaths({ global });
80820
- const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
80821
- const musecodeFrontmatter = {
80822
- name: rulesyncFrontmatter.name,
80823
- description: rulesyncFrontmatter.description
80824
- };
82956
+ const musecodeFrontmatter = buildMusecodeFrontmatter(rulesyncSkill.getFrontmatter());
80825
82957
  return new MusecodeSkill({
80826
82958
  outputRoot,
80827
82959
  relativeDirPath: settablePaths.relativeDirPath,
@@ -83272,8 +85404,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
83272
85404
  supportsProject: true,
83273
85405
  supportsSimulated: false,
83274
85406
  supportsGlobal: true,
83275
- lenientImport: true,
83276
- committedOutput: true
85407
+ lenientImport: true
83277
85408
  }
83278
85409
  }],
83279
85410
  ["grokcli", {
@@ -83886,10 +86017,6 @@ var SkillsProcessor = class extends DirFeatureProcessor {
83886
86017
  }
83887
86018
  async loadToolDirsToDelete() {
83888
86019
  const factory = this.getFactory(this.toolTarget);
83889
- if (factory.meta.committedOutput === true && !this.global) {
83890
- this.logger.debug(`Skipping the orphan sweep for ${this.toolTarget} skills: the project root is committed and user-owned`);
83891
- return [];
83892
- }
83893
86020
  const paths = factory.class.getSettablePaths({ global: this.global });
83894
86021
  const roots = toolSkillSearchRoots(paths);
83895
86022
  const toolSkills = [];
@@ -85434,6 +87561,14 @@ const PLUGIN_FORBIDDEN_FIELDS = [
85434
87561
  "mcpServers",
85435
87562
  "permissionMode"
85436
87563
  ];
87564
+ /**
87565
+ * Claude Code ignores these for plugin-shipped agents without any security
87566
+ * rationale. They are dropped with a separate warning so the output does not
87567
+ * suggest a behavior the plugin agent never gets.
87568
+ *
87569
+ * @see https://code.claude.com/docs/en/sub-agents#supported-frontmatter-fields
87570
+ */
87571
+ const PLUGIN_IGNORED_FIELDS = ["initialPrompt"];
85437
87572
  /** The only `isolation` value plugin agents accept. */
85438
87573
  const PLUGIN_ISOLATION_VALUE = "worktree";
85439
87574
  var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
@@ -85453,6 +87588,9 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
85453
87588
  const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
85454
87589
  for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
85455
87590
  if (dropped.length > 0) logger?.warn(`Dropping ${dropped.join(", ")} from claudecode-plugin subagent ${relativeFilePath}: Claude Code does not support these fields for plugin-shipped agents.`);
87591
+ const ignored = PLUGIN_IGNORED_FIELDS.filter((field) => sanitized[field] !== void 0);
87592
+ for (const field of PLUGIN_IGNORED_FIELDS) delete sanitized[field];
87593
+ if (ignored.length > 0) logger?.warn(`Dropping ${ignored.join(", ")} from claudecode-plugin subagent ${relativeFilePath}: Claude Code ignores these fields for plugin-shipped agents.`);
85456
87594
  if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
85457
87595
  logger?.warn(`Dropping isolation "${sanitized.isolation}" from claudecode-plugin subagent ${relativeFilePath}: "${PLUGIN_ISOLATION_VALUE}" is the only value Claude Code accepts for plugin-shipped agents.`);
85458
87596
  delete sanitized.isolation;
@@ -88906,6 +91044,32 @@ async function resolveOpenCodeAgentPrompt({ prompt, configDir }) {
88906
91044
  configDir
88907
91045
  });
88908
91046
  }
91047
+ /**
91048
+ * Resolves the `prompt` of each inline agent entry of one config file against
91049
+ * that file's directory, so entries from several files can be merged without
91050
+ * losing where a `"{file:./path}"` reference points.
91051
+ */
91052
+ async function resolveOpenCodeAgentPrompts({ entries, configDir }) {
91053
+ if (entries === null) return null;
91054
+ const resolved = {};
91055
+ for (const [name, rawEntry] of Object.entries(entries)) {
91056
+ const entry = asOpencodeEntries(rawEntry);
91057
+ const value = entry === null || !Object.hasOwn(entry, "prompt") ? rawEntry : {
91058
+ ...entry,
91059
+ prompt: await resolveOpenCodeAgentPrompt({
91060
+ prompt: entry.prompt,
91061
+ configDir
91062
+ })
91063
+ };
91064
+ Object.defineProperty(resolved, name, {
91065
+ value,
91066
+ enumerable: true,
91067
+ configurable: true,
91068
+ writable: true
91069
+ });
91070
+ }
91071
+ return resolved;
91072
+ }
88909
91073
  var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
88910
91074
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
88911
91075
  static layout = OPENCODE_LAYOUT;
@@ -88977,8 +91141,9 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
88977
91141
  });
88978
91142
  }
88979
91143
  /**
88980
- * Imports agents defined inline in `opencode.json` / `opencode.jsonc` under
88981
- * the top-level `agent` key — or its V2 spelling `agents`, see
91144
+ * Imports agents defined inline in `opencode.json` / `opencode.jsonc` (and,
91145
+ * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
91146
+ * them) under the top-level `agent` key — or its V2 spelling `agents`, see
88982
91147
  * `getOpencodeAgentEntries` — (in addition to the Markdown files under
88983
91148
  * `.opencode/agents/`). Each entry's `prompt` becomes the subagent body
88984
91149
  * (a `"{file:./path}"` reference is resolved relative to the config file's
@@ -88991,30 +91156,26 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
88991
91156
  * @see https://opencode.ai/docs/agents/#json
88992
91157
  */
88993
91158
  static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false } = {}) {
88994
- const agentEntries = getOpencodeAgentEntries({
88995
- config: await readOpencodeConfig({
88996
- outputRoot,
88997
- global,
88998
- layout: this.layout
88999
- }),
89000
- layout: this.layout
89001
- });
89002
- if (!agentEntries) return [];
89003
- const paths = this.getSettablePaths({ global });
89004
- const configDir = getOpencodeConfigDir({
91159
+ const layers = await readOpencodeConfigLayers({
89005
91160
  outputRoot,
89006
91161
  global,
89007
91162
  layout: this.layout
89008
91163
  });
91164
+ const agentEntries = mergeOpencodeEntryLayers(await Promise.all(layers.map(({ config, configDir }) => resolveOpenCodeAgentPrompts({
91165
+ entries: getOpencodeAgentEntries({
91166
+ config,
91167
+ layout: this.layout
91168
+ }),
91169
+ configDir
91170
+ }))));
91171
+ if (!agentEntries) return [];
91172
+ const paths = this.getSettablePaths({ global });
89009
91173
  const subagents = [];
89010
91174
  for (const [name, rawEntry] of Object.entries(agentEntries)) {
89011
91175
  const entry = asOpencodeEntries(rawEntry);
89012
91176
  if (!entry) continue;
89013
91177
  const { prompt, ...frontmatterFields } = entry;
89014
- const body = await resolveOpenCodeAgentPrompt({
89015
- prompt,
89016
- configDir
89017
- });
91178
+ const body = typeof prompt === "string" ? prompt : "";
89018
91179
  const parseResult = OpenCodeSubagentFrontmatterSchema.safeParse(frontmatterFields);
89019
91180
  if (!parseResult.success) continue;
89020
91181
  const frontmatter = parseResult.data;
@@ -91774,7 +93935,7 @@ var ToolRule = class extends ToolFile {
91774
93935
  body: this.getFileContent()
91775
93936
  });
91776
93937
  }
91777
- toRulesyncRuleDefault() {
93938
+ toRulesyncRuleDefault({ body } = {}) {
91778
93939
  return new RulesyncRule({
91779
93940
  outputRoot: process.cwd(),
91780
93941
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -91785,7 +93946,7 @@ var ToolRule = class extends ToolFile {
91785
93946
  description: this.description,
91786
93947
  globs: this.globs ?? (this.isRoot() ? ["**/*"] : [])
91787
93948
  },
91788
- body: this.getFileContent()
93949
+ body: body ?? this.getFileContent()
91789
93950
  });
91790
93951
  }
91791
93952
  isRoot() {
@@ -92891,11 +95052,19 @@ var AntigravityIdeRule = class extends ToolRule {
92891
95052
  }
92892
95053
  super({
92893
95054
  ...rest,
92894
- fileContent: rest.root ? body : stringifyFrontmatter(body, frontmatter)
95055
+ fileContent: rest.root && Object.keys(frontmatter).length === 0 ? body : stringifyFrontmatter(body, frontmatter)
92895
95056
  });
92896
95057
  this.frontmatter = frontmatter;
92897
95058
  this.body = body;
92898
95059
  }
95060
+ /**
95061
+ * Frontmatter written on the root rule. The project `AGENTS.md` and the
95062
+ * global `GEMINI.md` take none; a subclass whose root rule sits inside a
95063
+ * `rules/` directory, where every `.md` needs a `trigger`, overrides it.
95064
+ */
95065
+ static getRootRuleFrontmatter() {
95066
+ return {};
95067
+ }
92899
95068
  static getGlobalRootPath(excludeToolDir) {
92900
95069
  return {
92901
95070
  relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_GEMINI_DIR, ".", excludeToolDir),
@@ -92922,13 +95091,16 @@ var AntigravityIdeRule = class extends ToolRule {
92922
95091
  const paths = this.getSettablePaths({ global });
92923
95092
  if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
92924
95093
  const rootPath = paths.root;
92925
- const rootContent = await readFileContent((0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
95094
+ const rootFilePath = (0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath);
95095
+ const rootContent = await readFileContent(rootFilePath);
95096
+ const rootFrontmatter = this.getRootRuleFrontmatter();
95097
+ const rootBody = Object.keys(rootFrontmatter).length === 0 ? rootContent : parseFrontmatter(rootContent, rootFilePath).body;
92926
95098
  return new this({
92927
95099
  outputRoot,
92928
95100
  relativeDirPath: rootPath.relativeDirPath,
92929
95101
  relativeFilePath: rootPath.relativeFilePath,
92930
- frontmatter: {},
92931
- body: rootContent,
95102
+ frontmatter: rootFrontmatter,
95103
+ body: rootBody,
92932
95104
  validate,
92933
95105
  root: true,
92934
95106
  global
@@ -92988,7 +95160,7 @@ var AntigravityIdeRule = class extends ToolRule {
92988
95160
  outputRoot,
92989
95161
  relativeDirPath: rootPath.relativeDirPath,
92990
95162
  relativeFilePath: rootPath.relativeFilePath,
92991
- frontmatter: {},
95163
+ frontmatter: this.getRootRuleFrontmatter(),
92992
95164
  body: rulesyncRule.getBody(),
92993
95165
  validate,
92994
95166
  root: true,
@@ -93019,7 +95191,7 @@ var AntigravityIdeRule = class extends ToolRule {
93019
95191
  });
93020
95192
  }
93021
95193
  toRulesyncRule() {
93022
- if (this.root) return this.toRulesyncRuleDefault();
95194
+ if (this.root) return this.toRulesyncRuleDefault({ body: this.body });
93023
95195
  const strategy = STRATEGIES$1.find((s) => s.canHandle(this.frontmatter.trigger));
93024
95196
  let rulesyncData = {
93025
95197
  globs: [],
@@ -93097,6 +95269,9 @@ var AntigravityPluginRule = class extends AntigravityIdeRule {
93097
95269
  static supportsSubprojectRules() {
93098
95270
  return false;
93099
95271
  }
95272
+ static getRootRuleFrontmatter() {
95273
+ return { trigger: "always_on" };
95274
+ }
93100
95275
  static isTargetedByRulesyncRule(rulesyncRule) {
93101
95276
  const targets = rulesyncRule.getFrontmatter().targets;
93102
95277
  return targets.includes("*") || targets.includes("antigravity-plugin");
@@ -94686,8 +96861,11 @@ var CodexcliRule = class extends NestedAgentsmdRule {
94686
96861
  * `.codex/config.toml`, which Codex loads for trusted projects and which
94687
96862
  * overrides the 32 KiB default budget for project `AGENTS.md` files.
94688
96863
  *
94689
- * Answers `undefined` when the file is missing, is not valid TOML, or does not
94690
- * set the key to a positive integer, so the caller falls back to the default.
96864
+ * Answers `0` when the key is set to `0`, which Codex treats as "project docs
96865
+ * disabled" (`codex-rs/core/src/agents_md.rs` loads nothing for a zero
96866
+ * budget). Answers `undefined` when the file is missing, is not valid TOML, or
96867
+ * does not set the key to a non-negative integer, so the caller falls back to
96868
+ * the default.
94691
96869
  *
94692
96870
  * @see https://learn.chatgpt.com/docs/config-file/config-reference
94693
96871
  */
@@ -94702,7 +96880,7 @@ async function readCodexcliProjectDocMaxBytes(outputRoot) {
94702
96880
  }
94703
96881
  if (!isPlainObject$1(parsed)) return;
94704
96882
  const value = parsed.project_doc_max_bytes;
94705
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
96883
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
94706
96884
  }
94707
96885
  //#endregion
94708
96886
  //#region src/features/rules/commandcode-rule.ts
@@ -96064,10 +98242,13 @@ var DevinPluginRule = class extends DevinRule {
96064
98242
  * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
96065
98243
  * files — the project chain `dsh-agent-instructions` loads from the `.git`
96066
98244
  * root down to the session cwd, broad to specific, so a per-directory file
96067
- * applies only while working under that directory. Global scope writes the
96068
- * user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
96069
- * reads as a single root file with no `.local.md` overlay, so non-root rules
96070
- * fold into it.
98245
+ * applies only while working under that directory. A `localRoot` rule goes to
98246
+ * the project-root `AGENTS.local.md`, the additive local overlay the project
98247
+ * chain loads beside each `AGENTS.md`. Global scope writes the user-global
98248
+ * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a single
98249
+ * root file with no `.local.md` overlay, so non-root rules fold into it. The
98250
+ * harness also reads a shared `~/.agents/AGENTS.md` (`$DSH_AGENTS_HOME`) after
98251
+ * it, which rulesync never writes.
96071
98252
  *
96072
98253
  * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
96073
98254
  */
@@ -97738,10 +99919,6 @@ var OmpRule = class OmpRule extends ToolRule {
97738
99919
  }
97739
99920
  };
97740
99921
  //#endregion
97741
- //#region src/constants/openclaw-paths.ts
97742
- const OPENCLAW_WORKSPACE_DIR_PATH = (0, node_path.join)(".openclaw", "workspace");
97743
- const OPENCLAW_RULE_FILE_NAME = "AGENTS.md";
97744
- //#endregion
97745
99922
  //#region src/features/rules/openclaw-rule.ts
97746
99923
  var OpenclawRule = class OpenclawRule extends ToolRule {
97747
99924
  constructor({ fileContent, root, ...rest }) {
@@ -99879,6 +102056,39 @@ const normalizeRelativeDir = (dir) => node_path.posix.normalize(toPosixPath(dir)
99879
102056
  /** A tool rule's output path relative to its output root, for comparison. */
99880
102057
  const toolOutputPath = (toolRule) => node_path.posix.join(normalizeRelativeDir(toolRule.getRelativeDirPath()), toolRule.getRelativeFilePath());
99881
102058
  const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
102059
+ /**
102060
+ * The leading `---` frontmatter block (with its closing delimiter line) that
102061
+ * every one of `contents` starts with, or `undefined` when one lacks it or the
102062
+ * blocks differ.
102063
+ */
102064
+ const findSharedLeadingFrontmatter = (contents) => {
102065
+ const blocks = contents.map((content) => /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.exec(content)?.[0]);
102066
+ const first = blocks[0];
102067
+ return first !== void 0 && blocks.every((block) => block === first) ? first : void 0;
102068
+ };
102069
+ /**
102070
+ * Whether a group of rules colliding on one output path is composed into one
102071
+ * file, and the frontmatter block the composed file keeps, if any.
102072
+ */
102073
+ const decideRuleComposition = ({ group, collisionPolicy }) => {
102074
+ const contents = group.map(({ toolRule }) => toolRule.getFileContent());
102075
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
102076
+ const allFragmentsArePlain = contents.every((content) => !/^---\r?\n/.test(content));
102077
+ const sharedRootFrontmatter = allGeneratedRulesAreRoots ? findSharedLeadingFrontmatter(contents) : void 0;
102078
+ return {
102079
+ shouldCompose: (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain || sharedRootFrontmatter !== void 0,
102080
+ sharedRootFrontmatter
102081
+ };
102082
+ };
102083
+ /**
102084
+ * Concatenate colliding rule fragments. With `sharedFrontmatter` (the block
102085
+ * every fragment starts with), the block is stripped from each fragment and
102086
+ * written once at the top.
102087
+ */
102088
+ const composeRuleFragments = ({ contents, sharedFrontmatter }) => {
102089
+ const merged = contents.map((content) => content.slice(sharedFrontmatter?.length ?? 0).trim()).filter((content) => content.length > 0).join("\n\n");
102090
+ return sharedFrontmatter === void 0 ? merged : `${sharedFrontmatter}${merged}\n`;
102091
+ };
99882
102092
  const RulesFeatureOptionsSchema = zod_mini.z.looseObject({
99883
102093
  ruleDiscoveryMode: zod_mini.z.optional(zod_mini.z.enum(["none", "explicit"])),
99884
102094
  includeLocalRoot: zod_mini.z.optional(zod_mini.z.boolean()),
@@ -100078,7 +102288,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
100078
102288
  collisionPolicy: "fold",
100079
102289
  projectInstructionBudget: {
100080
102290
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
100081
- remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
102291
+ remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default): it silently truncates the file that crosses the limit and skips the files after it. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
100082
102292
  readConfiguredBytes: readCodexcliProjectDocMaxBytes,
100083
102293
  configuredSource: "`project_doc_max_bytes` in `.codex/config.toml` (honored only if the project is trusted)",
100084
102294
  defaultLimitHint: "Rulesync does not read `~/.codex/config.toml`; ignore this warning if you already raised `project_doc_max_bytes` there."
@@ -100488,7 +102698,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
100488
102698
  extension: "md",
100489
102699
  supportsGlobal: true,
100490
102700
  ruleDiscoveryMode: "auto",
100491
- collisionPolicy: "fold"
102701
+ collisionPolicy: "fold",
102702
+ localRootMode: "separate-local-file",
102703
+ localRootFileName: "AGENTS.local.md"
100492
102704
  }
100493
102705
  }]
100494
102706
  ]);
@@ -100650,14 +102862,22 @@ var RulesProcessor = class extends FeatureProcessor {
100650
102862
  * estimate can come out low there. The budget checked is the value set in the
100651
102863
  * output root's tool config when the tool supports one (Codex's
100652
102864
  * `project_doc_max_bytes` in `.codex/config.toml`), else the tool's default;
100653
- * the warning names which one it used. Global scope is not
100654
- * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
100655
- * project budget.
102865
+ * the warning names which one it used. A configured budget of `0` disables
102866
+ * project docs in Codex, so that case gets a single warning saying the
102867
+ * generated files will not be loaded instead of a size report. Global scope
102868
+ * is not checked because Codex loads the personal `~/.codex/AGENTS.md`
102869
+ * outside the project budget.
100656
102870
  */
100657
102871
  async warnForInstructionBudget({ toolRules, meta }) {
100658
102872
  const budget = meta.projectInstructionBudget;
100659
- if (budget === void 0 || this.global) return;
102873
+ if (budget === void 0 || this.global || toolRules.length === 0) return;
100660
102874
  const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
102875
+ const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
102876
+ if (configuredBytes === 0) {
102877
+ const paths = toolRules.map((rule) => toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
102878
+ this.logger.warn(`${budget.configuredSource ?? "The tool config"}${location} is set to 0, which disables ${this.toolTarget}'s project instruction files: the generated ${paths.join(", ")} will not be loaded. Remove the setting or set it to a positive byte count to load them.`);
102879
+ return;
102880
+ }
100661
102881
  const limitBytes = configuredBytes ?? budget.bytes;
100662
102882
  const limitLabel = budget.readConfiguredBytes === void 0 ? `${limitBytes}-byte limit` : configuredBytes === void 0 ? `${limitBytes}-byte default limit` : `${limitBytes}-byte limit set by ${budget.configuredSource ?? "the tool config"}`;
100663
102883
  const remedy = configuredBytes === void 0 && budget.defaultLimitHint !== void 0 ? `${budget.remedy} ${budget.defaultLimitHint}` : budget.remedy;
@@ -100667,7 +102887,6 @@ var RulesProcessor = class extends FeatureProcessor {
100667
102887
  bytes: Buffer.byteLength(addTrailingNewline(rule.getFileContent()), "utf8")
100668
102888
  }));
100669
102889
  const isAncestorOrSelf = (ancestor, dir) => ancestor === "." || ancestor === dir || dir.startsWith(`${ancestor}/`);
100670
- const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
100671
102890
  const rootFiles = files.filter(({ dir }) => dir === ".");
100672
102891
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100673
102892
  if (rootBytes > limitBytes) {
@@ -100975,10 +103194,11 @@ var RulesProcessor = class extends FeatureProcessor {
100975
103194
  continue;
100976
103195
  }
100977
103196
  const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
100978
- const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
100979
103197
  const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
100980
- const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
100981
- const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
103198
+ const { shouldCompose, sharedRootFrontmatter } = decideRuleComposition({
103199
+ group,
103200
+ collisionPolicy
103201
+ });
100982
103202
  if (!shouldCompose && hasSourceRoot) throw new Error(`Multiple generated rules resolve to output path '${path}' for target '${this.toolTarget}', but this target cannot safely compose a collision involving a root rule. Source rules: ${formatRulePaths(group.map(({ rulesyncRule }) => rulesyncRule))}`);
100983
103203
  if (!shouldCompose) {
100984
103204
  for (const conversion of group) survivors.add(conversion);
@@ -100986,8 +103206,11 @@ var RulesProcessor = class extends FeatureProcessor {
100986
103206
  }
100987
103207
  const target = rootConversion ?? group[0];
100988
103208
  if (!target) continue;
100989
- const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
100990
- target.toolRule.setFileContent(mergedContent);
103209
+ const ordered = [target, ...group.filter((rule) => rule !== target)];
103210
+ target.toolRule.setFileContent(composeRuleFragments({
103211
+ contents: ordered.map(({ toolRule }) => toolRule.getFileContent()),
103212
+ sharedFrontmatter: sharedRootFrontmatter
103213
+ }));
100991
103214
  survivors.add(target);
100992
103215
  }
100993
103216
  for (let i = convertedRules.length - 1; i >= 0; i--) {
@@ -101071,7 +103294,7 @@ var RulesProcessor = class extends FeatureProcessor {
101071
103294
  /**
101072
103295
  * Handle localRoot rule generation based on tool target.
101073
103296
  * - `separate-local-file`: writes a dedicated `*.local.md` root file
101074
- * (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
103297
+ * (claudecode/legacy: `./CLAUDE.local.md`, rovodev/dsh: `./AGENTS.local.md`)
101075
103298
  * - `append-to-root` (default): appends the body to the root file
101076
103299
  */
101077
103300
  handleLocalRootRule(toolRules, localRootRule, factory) {
@@ -101187,7 +103410,11 @@ var RulesProcessor = class extends FeatureProcessor {
101187
103410
  localRoot
101188
103411
  });
101189
103412
  }
101190
- const workingRootClass = [CrushRule, ReasonixRule].find((base) => isClassOrSubclassOf({
103413
+ const workingRootClass = [
103414
+ CrushRule,
103415
+ ReasonixRule,
103416
+ DshRule
103417
+ ].find((base) => isClassOrSubclassOf({
101191
103418
  candidate: factory.class,
101192
103419
  base
101193
103420
  }));