rulesync 34.0.1 → 36.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$418 } = node_path.posix;
460
+ const { join: join$422 } = 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$418(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$422(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$418(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$418(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$422(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$422(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
484
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
485
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$422(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$418(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
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";
@@ -605,6 +605,7 @@ const ignoreProcessorToolTargetTuple = [
605
605
  "reasonix",
606
606
  "roo",
607
607
  "tabnine",
608
+ "trae",
608
609
  "devin",
609
610
  "vibe",
610
611
  "warp",
@@ -698,6 +699,7 @@ const commandsProcessorToolTargetTuple = [
698
699
  "hermesagent",
699
700
  "junie",
700
701
  "kilo",
702
+ "kimi-code",
701
703
  "kimi-code-plugin",
702
704
  "kiro",
703
705
  "kiro-cli",
@@ -714,6 +716,7 @@ const commandsProcessorToolTargetTuple = [
714
716
  "rovodev",
715
717
  "tabnine",
716
718
  "takt",
719
+ "trae",
717
720
  "devin",
718
721
  "warp",
719
722
  "zcode",
@@ -1405,20 +1408,22 @@ async function fileExists(filepath) {
1405
1408
  return false;
1406
1409
  }
1407
1410
  }
1408
- async function getFileSize(filepath) {
1411
+ /**
1412
+ * The size of a file and whether it carries any execute bit, read with a single
1413
+ * `stat`. Windows reports no execute bits, so a file there is never seen as
1414
+ * executable.
1415
+ */
1416
+ async function getFileSizeAndExecutableBit(filepath) {
1409
1417
  try {
1410
- return (await (0, node_fs_promises.stat)(filepath)).size;
1418
+ const stats = await (0, node_fs_promises.stat)(filepath);
1419
+ return {
1420
+ size: stats.size,
1421
+ executable: (stats.mode & 73) !== 0
1422
+ };
1411
1423
  } catch (error) {
1412
- throw new Error(`Failed to get file size for "${filepath}": ${formatError(error)}`, { cause: error });
1424
+ throw new Error(`Failed to stat "${filepath}": ${formatError(error)}`, { cause: error });
1413
1425
  }
1414
1426
  }
1415
- /**
1416
- * Whether a file carries any execute bit. Windows reports none, so a file
1417
- * there is never seen as executable.
1418
- */
1419
- async function hasExecutableBit(filepath) {
1420
- return ((await (0, node_fs_promises.stat)(filepath)).mode & 73) !== 0;
1421
- }
1422
1427
  async function isSymlink(filepath) {
1423
1428
  try {
1424
1429
  return (await (0, node_fs_promises.lstat)(filepath)).isSymbolicLink();
@@ -5289,7 +5294,7 @@ const JUNIE_HOOK_EVENTS = [
5289
5294
  * `PreToolUse`, and on the end reason for `SessionEnd`. Only `type: "command"`
5290
5295
  * hooks are supported.
5291
5296
  * @see https://docs.gitlab.com/user/gitlab_duo_cli/customize/
5292
- * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/core/hooks/src/schema.ts
5297
+ * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/v9.28.0/packages/core/hooks/src/schema.ts
5293
5298
  */
5294
5299
  const GITLABDUO_HOOK_EVENTS = [
5295
5300
  "sessionStart",
@@ -5643,6 +5648,7 @@ const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
5643
5648
  *
5644
5649
  * @see https://www.codebuddy.ai/docs/cli/hooks
5645
5650
  * @see https://www.codebuddy.ai/docs/cli/plugins-reference
5651
+ * @see https://www.npmjs.com/package/@tencent-ai/codebuddy-code/v/2.167.0
5646
5652
  */
5647
5653
  const CODEBUDDY_HOOK_EVENTS = [
5648
5654
  "sessionStart",
@@ -8168,7 +8174,8 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8168
8174
  * autonomy/sandbox controls with no canonical permission category — under
8169
8175
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
8170
8176
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
8171
- * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
8177
+ * `todoWrite`, `codeModeOnly`, `freeform`, `workflowsEnabled`, `workflowNameOnly`,
8178
+ * `executionSandbox`) and
8172
8179
  * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
8173
8180
  * `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
8174
8181
  * `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
@@ -8187,7 +8194,8 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8187
8194
  * changes what the file said is reported in either scope, naming what that key
8188
8195
  * decides there — the autonomy and containment
8189
8196
  * controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
8190
- * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
8197
+ * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`,
8198
+ * `codeModeOnly`, `freeform`, and `eager`, which
8191
8199
  * demotes an omitted tool to deferred rather than removing it, replacing rather
8192
8200
  * than merging with the list a higher scope set), the Auto Mode
8193
8201
  * classifier config, the skill kill-switches, and, because these groups are
@@ -10880,13 +10888,15 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
10880
10888
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
10881
10889
  license: zod_mini.z.optional(zod_mini.z.unknown()),
10882
10890
  compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
10883
- metadata: zod_mini.z.optional(zod_mini.z.unknown())
10891
+ metadata: zod_mini.z.optional(zod_mini.z.unknown()),
10892
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
10884
10893
  })),
10885
10894
  mimocode: zod_mini.z.optional(zod_mini.z.looseObject({
10886
10895
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
10887
10896
  license: zod_mini.z.optional(zod_mini.z.unknown()),
10888
10897
  compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
10889
- metadata: zod_mini.z.optional(zod_mini.z.unknown())
10898
+ metadata: zod_mini.z.optional(zod_mini.z.unknown()),
10899
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
10890
10900
  })),
10891
10901
  kilo: zod_mini.z.optional(zod_mini.z.looseObject({
10892
10902
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -11339,7 +11349,7 @@ async function getLocalSkillDirNames(sourceTree) {
11339
11349
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
11340
11350
  * default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11341
11351
  * copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
11342
- * commandcode, lettacode). Each tool's own section may override that
11352
+ * commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
11343
11353
  * default with a per-target value. A defined section value (including `false`)
11344
11354
  * always wins over the root default.
11345
11355
  *
@@ -13538,8 +13548,8 @@ function diffSharedConfigOwnedKeys({ fileKey, existingContent, newContent }) {
13538
13548
  return operations;
13539
13549
  }
13540
13550
  const READ_TOOL_NAME = "Read";
13541
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
13542
- const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
13551
+ const isReadDenyEntry = (entry, readToolName = READ_TOOL_NAME) => entry.startsWith(`${readToolName}(`) && entry.endsWith(")");
13552
+ const buildReadDenyEntry = (pattern, readToolName = READ_TOOL_NAME) => `${readToolName}(${pattern})`;
13543
13553
  const parsePermissionsBlock = (settings) => {
13544
13554
  const permissions = settings.permissions ?? {};
13545
13555
  return {
@@ -13563,9 +13573,9 @@ const withPermissions = (settings, next) => {
13563
13573
  };
13564
13574
  };
13565
13575
  const applyIgnoreReadDenies = (params) => {
13566
- const { settings, readDenies } = params;
13576
+ const { settings, readDenies, readToolName = READ_TOOL_NAME } = params;
13567
13577
  const current = parsePermissionsBlock(settings);
13568
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
13578
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry, readToolName) || readDenies.includes(entry));
13569
13579
  return withPermissions(settings, {
13570
13580
  allow: current.allow,
13571
13581
  ask: current.ask,
@@ -17892,6 +17902,7 @@ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
17892
17902
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
17893
17903
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
17894
17904
  const ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME = "plugin.json";
17905
+ const ANTIGRAVITY_PLUGIN_MANIFEST_FOLDERS = [".antigravity-plugin", ".gemini-plugin"];
17895
17906
  const AUGMENTCODE_PLUGIN_RULES_DIR = "rules";
17896
17907
  const AUGMENTCODE_PLUGIN_COMMANDS_DIR = "commands";
17897
17908
  const AUGMENTCODE_PLUGIN_AGENTS_DIR = "agents";
@@ -19538,9 +19549,36 @@ function commandSlug(relativeFilePath) {
19538
19549
  * skill. Once the command is removed from every source tree's `commands/`
19539
19550
  * directory, the directory stops matching and the skills feature cleans
19540
19551
  * it up as a regular orphan.
19552
+ *
19553
+ * When `toolTarget` is given, only commands whose `targets` include it (or
19554
+ * `*`) count: a command aimed at other tools never reaches this tool's skills
19555
+ * tree, so it must not shadow a same-named skill there. Commands that cannot
19556
+ * be parsed are skipped, as the commands feature reports them itself.
19541
19557
  */
19542
- async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
19543
- return (await Promise.all(inputRoots.map((root) => findFilesByGlobs("**/*.md", { cwd: (0, node_path.join)(root, COMMANDS_FEATURE_SUBDIR) })))).flat().some((filePath) => commandSlug((0, node_path.basename)(filePath)) === dirName);
19558
+ async function rulesyncCommandSlugExists({ inputRoots, dirName, toolTarget }) {
19559
+ const matching = (await Promise.all(inputRoots.map(async (root) => {
19560
+ const commandsDirPath = (0, node_path.join)(root, COMMANDS_FEATURE_SUBDIR);
19561
+ return (await findFilesByGlobs("**/*.md", { cwd: commandsDirPath })).map((filePath) => ({
19562
+ root,
19563
+ commandsDirPath,
19564
+ filePath
19565
+ }));
19566
+ }))).flat().filter(({ filePath }) => commandSlug((0, node_path.basename)(filePath)) === dirName);
19567
+ if (toolTarget === void 0) return matching.length > 0;
19568
+ for (const { root, commandsDirPath, filePath } of matching) {
19569
+ let targets;
19570
+ try {
19571
+ targets = (await RulesyncCommand.fromFile({
19572
+ outputRoot: root,
19573
+ relativeDirPath: COMMANDS_FEATURE_SUBDIR,
19574
+ relativeFilePath: (0, node_path.relative)(commandsDirPath, filePath)
19575
+ })).getFrontmatter().targets;
19576
+ } catch {
19577
+ continue;
19578
+ }
19579
+ if (targets.includes("*") || targets.includes(toolTarget)) return true;
19580
+ }
19581
+ return false;
19544
19582
  }
19545
19583
  //#endregion
19546
19584
  //#region src/features/commands/devin-command.ts
@@ -19558,7 +19596,7 @@ const DEVIN_COMMAND_SECTION_KEYS = [
19558
19596
  "permissions",
19559
19597
  "triggers"
19560
19598
  ];
19561
- function commandSkillContent$1(rulesyncCommand) {
19599
+ function commandSkillContent$2(rulesyncCommand) {
19562
19600
  const slug = commandSlug(rulesyncCommand.getRelativeFilePath());
19563
19601
  const frontmatter = rulesyncCommand.getFrontmatter();
19564
19602
  const description = frontmatter.description ?? `${slug} command`;
@@ -19632,7 +19670,7 @@ var DevinCommand = class DevinCommand extends ToolCommand {
19632
19670
  relativeDirPath: paths.relativeDirPath,
19633
19671
  relativeFilePath: SKILL_FILE_NAME,
19634
19672
  slug: commandSlug(rulesyncCommand.getRelativeFilePath()),
19635
- fileContent: commandSkillContent$1(rulesyncCommand)
19673
+ fileContent: commandSkillContent$2(rulesyncCommand)
19636
19674
  });
19637
19675
  }
19638
19676
  getFileContent() {
@@ -21452,6 +21490,180 @@ var KiloCommand = class KiloCommand extends ToolCommand {
21452
21490
  }
21453
21491
  };
21454
21492
  //#endregion
21493
+ //#region src/constants/kimi-code-paths.ts
21494
+ const KIMI_CODE_DIR = ".kimi-code";
21495
+ const KIMI_CODE_RULE_FILE_NAME = "AGENTS.md";
21496
+ const KIMI_CODE_MCP_FILE_NAME = "mcp.json";
21497
+ const KIMI_CODE_CONFIG_FILE_NAME = "config.toml";
21498
+ const KIMI_CODE_SKILLS_DIR_NAME = "skills";
21499
+ const KIMI_CODE_AGENTS_DIR_NAME = "agents";
21500
+ const KIMI_CODE_SKILLS_DIR_PATH = (0, node_path.join)(KIMI_CODE_DIR, KIMI_CODE_SKILLS_DIR_NAME);
21501
+ (0, node_path.join)(KIMI_CODE_DIR, KIMI_CODE_AGENTS_DIR_NAME);
21502
+ const KIMI_CODE_SHARED_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
21503
+ const KIMI_CODE_SHARED_AGENTS_DIR_PATH = (0, node_path.join)(".agents", "agents");
21504
+ //#endregion
21505
+ //#region src/utils/kimi-code.ts
21506
+ function getKimiCodeHome() {
21507
+ const configuredHome = process.env.KIMI_CODE_HOME?.trim();
21508
+ return configuredHome ? (0, node_path.resolve)(configuredHome) : void 0;
21509
+ }
21510
+ function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
21511
+ return global && getKimiCodeHome() ? relativeDirPath : (0, node_path.join)(KIMI_CODE_DIR, relativeDirPath);
21512
+ }
21513
+ /**
21514
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
21515
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
21516
+ * Declared unconditionally so the derived shared-file keys — and the drift
21517
+ * guards checked against them — do not depend on the ambient environment.
21518
+ */
21519
+ function getKimiCodeSharedConfigWritePaths() {
21520
+ return [{
21521
+ relativeDirPath: KIMI_CODE_DIR,
21522
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21523
+ }, {
21524
+ relativeDirPath: ".",
21525
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21526
+ }];
21527
+ }
21528
+ /**
21529
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
21530
+ * Both spellings carry the same declaration, but passing the key of the file
21531
+ * being written keeps the write path and the drift guards on the same entry.
21532
+ */
21533
+ function getKimiCodeConfigSharedFileKey({ global }) {
21534
+ return sharedConfigFileKey({
21535
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
21536
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21537
+ });
21538
+ }
21539
+ function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
21540
+ return getToolRulesyncOutputRoot({
21541
+ nativeOutputRoot,
21542
+ global,
21543
+ toolHome: getKimiCodeHome
21544
+ });
21545
+ }
21546
+ //#endregion
21547
+ //#region src/features/commands/kimi-code-command.ts
21548
+ /**
21549
+ * Kimi Code SKILL.md frontmatter keys a command may author through its
21550
+ * `kimi-code:` section, each with the alias spellings Kimi also accepts. An
21551
+ * alias is normalized to the canonical key; the canonical spelling wins when
21552
+ * both are present.
21553
+ * @see https://moonshotai.github.io/kimi-code/en/customization/skills.html
21554
+ */
21555
+ const KIMI_CODE_COMMAND_SECTION_KEYS = [
21556
+ {
21557
+ key: "type",
21558
+ aliases: []
21559
+ },
21560
+ {
21561
+ key: "whenToUse",
21562
+ aliases: ["when-to-use", "when_to_use"]
21563
+ },
21564
+ {
21565
+ key: "disableModelInvocation",
21566
+ aliases: ["disable-model-invocation", "disable_model_invocation"]
21567
+ },
21568
+ {
21569
+ key: "arguments",
21570
+ aliases: []
21571
+ }
21572
+ ];
21573
+ function pickKimiCodeSectionKeys(section) {
21574
+ const picked = {};
21575
+ for (const { key, aliases } of KIMI_CODE_COMMAND_SECTION_KEYS) {
21576
+ const spelling = [key, ...aliases].find((candidate) => section[candidate] !== void 0);
21577
+ if (spelling !== void 0) picked[key] = section[spelling];
21578
+ }
21579
+ return picked;
21580
+ }
21581
+ function commandSkillContent$1(rulesyncCommand) {
21582
+ const slug = commandSlug(rulesyncCommand.getRelativeFilePath());
21583
+ const frontmatter = rulesyncCommand.getFrontmatter();
21584
+ const description = frontmatter.description?.trim() || `${slug} command`;
21585
+ return stringifyFrontmatter(rulesyncCommand.getBody().trim(), {
21586
+ name: slug,
21587
+ description,
21588
+ disableModelInvocation: true,
21589
+ ...pickKimiCodeSectionKeys(frontmatter["kimi-code"] ?? {})
21590
+ });
21591
+ }
21592
+ /**
21593
+ * Represents a Kimi Code slash command, emitted as a Kimi Code Skill.
21594
+ *
21595
+ * Kimi Code has no standalone custom-command directory outside plugins: every
21596
+ * external Skill is registered as a slash command (`/skill:<name>`, with a
21597
+ * `/<name>` shorthand when the bare name is free), and the Skill body supports
21598
+ * `$ARGUMENTS`, positional `$0` / `$1`, and named `$<name>` placeholders.
21599
+ * Commands are therefore emitted onto the native skills surface, one
21600
+ * `SKILL.md` per command: `.kimi-code/skills/<slug>/SKILL.md` (project) and
21601
+ * `~/.kimi-code/skills/<slug>/SKILL.md` (global; `$KIMI_CODE_HOME/skills/`
21602
+ * when `KIMI_CODE_HOME` is set). Directory-form `SKILL.md` requires both
21603
+ * `name` and `description`, so both are always written.
21604
+ *
21605
+ * Import and deletion are intentionally no-ops for this target: the skills
21606
+ * feature owns the `.kimi-code/skills/` tree, so importing it as commands
21607
+ * would double-import every skill (mirrors the Devin, Warp and Hermes Agent
21608
+ * commands targets).
21609
+ *
21610
+ * @see https://moonshotai.github.io/kimi-code/en/customization/skills.html
21611
+ * @see https://moonshotai.github.io/kimi-code/en/reference/slash-commands.html
21612
+ */
21613
+ var KimiCodeCommand = class KimiCodeCommand extends ToolCommand {
21614
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
21615
+ return this.isTargetedByRulesyncCommandDefault({
21616
+ rulesyncCommand,
21617
+ toolTarget: "kimi-code"
21618
+ });
21619
+ }
21620
+ static getSettablePaths({ global = false } = {}) {
21621
+ return { relativeDirPath: getKimiCodeRelativeDirPath({
21622
+ global,
21623
+ relativeDirPath: KIMI_CODE_SKILLS_DIR_NAME
21624
+ }) };
21625
+ }
21626
+ constructor({ slug, ...params }) {
21627
+ super({
21628
+ ...params,
21629
+ ...slug !== void 0 && {
21630
+ relativeDirPath: (0, node_path.join)(params.relativeDirPath, slug),
21631
+ relativeFilePath: "SKILL.md"
21632
+ }
21633
+ });
21634
+ }
21635
+ validate() {
21636
+ return {
21637
+ success: true,
21638
+ error: null
21639
+ };
21640
+ }
21641
+ toRulesyncCommand() {
21642
+ const slug = (0, node_path.basename)((0, node_path.dirname)(this.getRelativePathFromCwd()));
21643
+ const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getFilePath());
21644
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
21645
+ return new RulesyncCommand({
21646
+ relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
21647
+ relativeFilePath: `${slug}.md`,
21648
+ frontmatter: { description },
21649
+ body: body.trimStart()
21650
+ });
21651
+ }
21652
+ static fromRulesyncCommand({ outputRoot, rulesyncCommand, global = false }) {
21653
+ const paths = KimiCodeCommand.getSettablePaths({ global });
21654
+ return new KimiCodeCommand({
21655
+ outputRoot,
21656
+ relativeDirPath: paths.relativeDirPath,
21657
+ relativeFilePath: SKILL_FILE_NAME,
21658
+ slug: commandSlug(rulesyncCommand.getRelativeFilePath()),
21659
+ fileContent: commandSkillContent$1(rulesyncCommand)
21660
+ });
21661
+ }
21662
+ getFileContent() {
21663
+ return this.fileContent;
21664
+ }
21665
+ };
21666
+ //#endregion
21455
21667
  //#region src/features/commands/kimi-code-plugin-command.ts
21456
21668
  /**
21457
21669
  * Kimi Code reads only `name` and `description` from a plugin command. The
@@ -21606,6 +21818,14 @@ const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
21606
21818
  const KIRO_MCP_FILE_NAME = "mcp.json";
21607
21819
  const KIRO_IGNORE_FILE_NAME = ".kiroignore";
21608
21820
  const KIRO_GLOBAL_IGNORE_FILE_NAME = "kiroignore";
21821
+ /**
21822
+ * Kiro's capability-based permission rules (IDE 1.0, CLI V3). The user-level
21823
+ * file lives at `~/.kiro/settings/permissions.yaml`; the workspace-level file
21824
+ * sits under a hash-derived `~/.kiro/workspace-roots/<hash>/` directory outside
21825
+ * the repository, so only the user-level file is generated.
21826
+ * @see https://kiro.dev/docs/permissions/
21827
+ */
21828
+ const KIRO_PERMISSIONS_FILE_NAME = "permissions.yaml";
21609
21829
  //#endregion
21610
21830
  //#region src/features/commands/kiro-command.ts
21611
21831
  var KiroCommand = class KiroCommand extends ToolCommand {
@@ -23618,6 +23838,193 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23618
23838
  }
23619
23839
  };
23620
23840
  //#endregion
23841
+ //#region src/constants/trae-paths.ts
23842
+ /**
23843
+ * Trae (ByteDance's AI IDE, "TraeCode" in its current docs) keeps project
23844
+ * configuration under `.trae/` and user configuration under `~/.trae/`.
23845
+ *
23846
+ * @see https://docs.trae.ai/ide/rules?_lang=en
23847
+ * @see https://docs.trae.ai/ide/skills?_lang=en
23848
+ * @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
23849
+ * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23850
+ * @see https://docs.trae.ai/ide/ignore-files?_lang=en
23851
+ */
23852
+ const TRAE_DIR = ".trae";
23853
+ /** Project MCP servers (`mcpServers` object), project scope only. */
23854
+ const TRAE_MCP_FILE_NAME = "mcp.json";
23855
+ /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
23856
+ const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
23857
+ /**
23858
+ * Custom commands: Markdown files under `.trae/commands/` (project, up to three nested
23859
+ * directory levels) and `~/.trae/commands/` (global).
23860
+ */
23861
+ const TRAE_COMMANDS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "commands");
23862
+ /** Indexing ignore list (gitignore syntax), `.trae/.ignore`, project scope only. */
23863
+ const TRAE_IGNORE_FILE_NAME = ".ignore";
23864
+ //#endregion
23865
+ //#region src/features/commands/trae-command.ts
23866
+ const TraeCommandFrontmatterSchema = zod_mini.z.looseObject({
23867
+ description: zod_mini.z.optional(zod_mini.z.string()),
23868
+ name: zod_mini.z.optional(zod_mini.z.string())
23869
+ });
23870
+ /**
23871
+ * Trae reads project commands only up to three directory levels below
23872
+ * `.trae/commands/`; a command file nested deeper is never picked up.
23873
+ */
23874
+ const TRAE_MAX_PROJECT_COMMAND_DIR_DEPTH = 3;
23875
+ /**
23876
+ * Custom command for Trae.
23877
+ *
23878
+ * Trae reads Markdown command files under `.trae/commands/` (project, up to
23879
+ * three levels of nested directories) and `~/.trae/commands/` (global). The
23880
+ * YAML frontmatter carries `name` and `description`, and the body below the
23881
+ * closing `---` holds the instructions. rulesync writes the rulesync
23882
+ * `description`, a `name` equal to the file name stem (as Trae's editor does),
23883
+ * plus any fields of the `trae` section (which win over the derived ones), and
23884
+ * keeps nested command paths.
23885
+ *
23886
+ * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23887
+ */
23888
+ var TraeCommand = class TraeCommand extends ToolCommand {
23889
+ frontmatter;
23890
+ body;
23891
+ constructor({ frontmatter, body, ...rest }) {
23892
+ if (rest.validate) {
23893
+ const result = TraeCommandFrontmatterSchema.safeParse(frontmatter);
23894
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
23895
+ }
23896
+ super({
23897
+ ...rest,
23898
+ fileContent: stringifyFrontmatter(body, frontmatter)
23899
+ });
23900
+ this.frontmatter = frontmatter;
23901
+ this.body = body;
23902
+ }
23903
+ static getSettablePaths(_options = {}) {
23904
+ return { relativeDirPath: TRAE_COMMANDS_DIR_PATH };
23905
+ }
23906
+ getBody() {
23907
+ return this.body;
23908
+ }
23909
+ getFrontmatter() {
23910
+ return this.frontmatter;
23911
+ }
23912
+ toRulesyncCommand() {
23913
+ const { description, name, ...otherFields } = this.frontmatter;
23914
+ const restFields = name === void 0 || name === (0, node_path.basename)(this.relativeFilePath, ".md") ? otherFields : {
23915
+ name,
23916
+ ...otherFields
23917
+ };
23918
+ const rulesyncFrontmatter = {
23919
+ targets: ["*"],
23920
+ description,
23921
+ ...Object.keys(restFields).length > 0 && { trae: restFields }
23922
+ };
23923
+ return new RulesyncCommand({
23924
+ outputRoot: ".",
23925
+ frontmatter: rulesyncFrontmatter,
23926
+ body: this.body,
23927
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
23928
+ relativeFilePath: this.relativeFilePath,
23929
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
23930
+ validate: true
23931
+ });
23932
+ }
23933
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
23934
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
23935
+ const { name: _name, ...traeFields } = rulesyncFrontmatter.trae ?? {};
23936
+ const traeFrontmatter = {
23937
+ name: this.deriveName(rulesyncCommand),
23938
+ description: rulesyncFrontmatter.description,
23939
+ ...traeFields
23940
+ };
23941
+ return new TraeCommand({
23942
+ outputRoot,
23943
+ frontmatter: traeFrontmatter,
23944
+ body: rulesyncCommand.getBody(),
23945
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
23946
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
23947
+ validate
23948
+ });
23949
+ }
23950
+ validate() {
23951
+ if (!this.frontmatter) return {
23952
+ success: true,
23953
+ error: null
23954
+ };
23955
+ const result = TraeCommandFrontmatterSchema.safeParse(this.frontmatter);
23956
+ if (result.success) return {
23957
+ success: true,
23958
+ error: null
23959
+ };
23960
+ return {
23961
+ success: false,
23962
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
23963
+ };
23964
+ }
23965
+ /**
23966
+ * Warn about commands Trae will not load as written: Trae documents `name`
23967
+ * as the command's unique identifier, so two commands deriving the same
23968
+ * `name` (e.g. `a/review.md` and `b/review.md`) collide, and it reads
23969
+ * project commands only up to three nested directory levels. Both files are
23970
+ * still written, so this warns rather than failing the run.
23971
+ */
23972
+ static validateRulesyncCommands({ rulesyncCommands, logger, global = false }) {
23973
+ const pathsByName = /* @__PURE__ */ new Map();
23974
+ for (const rulesyncCommand of rulesyncCommands) {
23975
+ if (!this.isTargetedByRulesyncCommand(rulesyncCommand)) continue;
23976
+ const relativeFilePath = toPosixPath(rulesyncCommand.getRelativeFilePath());
23977
+ const name = this.deriveName(rulesyncCommand);
23978
+ if (typeof name === "string") pathsByName.set(name, [...pathsByName.get(name) ?? [], relativeFilePath]);
23979
+ const dirDepth = relativeFilePath.split("/").length - 1;
23980
+ if (!global && dirDepth > TRAE_MAX_PROJECT_COMMAND_DIR_DEPTH) logger.warn(`Trae reads project commands only up to ${TRAE_MAX_PROJECT_COMMAND_DIR_DEPTH} nested directory levels, so ${quoteForLog(relativeFilePath)} is written but never loaded. Move it to a shallower directory.`);
23981
+ }
23982
+ for (const [name, paths] of pathsByName) {
23983
+ if (paths.length < 2) continue;
23984
+ logger.warn(`Trae command name ${quoteForLog(name)} is shared by ${paths.map(quoteForLog).join(", ")}, but Trae treats \`name\` as a unique identifier. Rename a file or set \`trae.name\` to tell them apart.`);
23985
+ }
23986
+ }
23987
+ /**
23988
+ * The `name` Trae sees for a command: the `trae` section's `name` when set,
23989
+ * otherwise the file name stem. A non-string override is returned as is so
23990
+ * frontmatter validation still rejects it.
23991
+ */
23992
+ static deriveName(rulesyncCommand) {
23993
+ return rulesyncCommand.getFrontmatter().trae?.name ?? (0, node_path.basename)(rulesyncCommand.getRelativeFilePath(), ".md");
23994
+ }
23995
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
23996
+ return this.isTargetedByRulesyncCommandDefault({
23997
+ rulesyncCommand,
23998
+ toolTarget: "trae"
23999
+ });
24000
+ }
24001
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
24002
+ const paths = this.getSettablePaths({ global });
24003
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
24004
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
24005
+ const result = TraeCommandFrontmatterSchema.safeParse(frontmatter);
24006
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
24007
+ return new TraeCommand({
24008
+ outputRoot,
24009
+ relativeDirPath: paths.relativeDirPath,
24010
+ relativeFilePath,
24011
+ frontmatter: result.data,
24012
+ body: body.trim(),
24013
+ validate
24014
+ });
24015
+ }
24016
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
24017
+ return new TraeCommand({
24018
+ outputRoot,
24019
+ relativeDirPath,
24020
+ relativeFilePath,
24021
+ frontmatter: { description: "" },
24022
+ body: "",
24023
+ validate: false
24024
+ });
24025
+ }
24026
+ };
24027
+ //#endregion
23621
24028
  //#region src/constants/warp-paths.ts
23622
24029
  const WARP_DIR = ".warp";
23623
24030
  const WARP_SKILLS_DIR_PATH = (0, node_path.join)(WARP_DIR, "skills");
@@ -24168,6 +24575,17 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24168
24575
  supportsSubdirectory: false
24169
24576
  }
24170
24577
  }],
24578
+ ["kimi-code", {
24579
+ class: KimiCodeCommand,
24580
+ meta: {
24581
+ extension: "md",
24582
+ supportsProject: true,
24583
+ supportsGlobal: true,
24584
+ isSimulated: false,
24585
+ supportsSubdirectory: false,
24586
+ skipToolFileScan: true
24587
+ }
24588
+ }],
24171
24589
  ["kimi-code-plugin", {
24172
24590
  class: KimiCodePluginCommand,
24173
24591
  meta: {
@@ -24328,6 +24746,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24328
24746
  supportsSubdirectory: false
24329
24747
  }
24330
24748
  }],
24749
+ ["trae", {
24750
+ class: TraeCommand,
24751
+ meta: {
24752
+ extension: "md",
24753
+ supportsProject: true,
24754
+ supportsGlobal: true,
24755
+ isSimulated: false,
24756
+ supportsSubdirectory: true
24757
+ }
24758
+ }],
24331
24759
  ["devin", {
24332
24760
  class: DevinCommand,
24333
24761
  meta: {
@@ -24411,7 +24839,8 @@ var CommandsProcessor = class extends FeatureProcessor {
24411
24839
  await factory.class.validateRulesyncCommands?.({
24412
24840
  inputRoots: this.inputRoots,
24413
24841
  rulesyncCommands,
24414
- logger: this.logger
24842
+ logger: this.logger,
24843
+ global: this.global
24415
24844
  });
24416
24845
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
24417
24846
  const toolCommands = [];
@@ -26091,8 +26520,8 @@ function isSupportedHookType({ type, converterConfig }) {
26091
26520
  * side either way, so the value is dropped rather than carried into a config
26092
26521
  * the tool would ignore or refuse.
26093
26522
  */
26094
- function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }) {
26095
- const { canonical, tool, commandOnly, events } = spec;
26523
+ function isKeywordFieldApplicable({ spec, hookType, eventName, flags, direction, warn }) {
26524
+ const { canonical, tool, commandOnly, hookTypes, events, ignoredEvents, ignoredWhenFlags } = spec;
26096
26525
  const field = direction === "generated" ? canonical : tool;
26097
26526
  if (!isFieldApplicable({
26098
26527
  commandOnly,
@@ -26101,10 +26530,23 @@ function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }
26101
26530
  warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not ${direction}.`);
26102
26531
  return false;
26103
26532
  }
26533
+ if (hookTypes !== void 0 && !hookTypes.has(hookType)) {
26534
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on ${[...hookTypes].map((type) => `"${type}"`).join(", ")} hooks only, so it is not ${direction}.`);
26535
+ return false;
26536
+ }
26104
26537
  if (events !== void 0 && !events.has(eventName)) {
26105
26538
  warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool honours "${tool}" on ${[...events].map((event) => `"${event}"`).join(", ")} only, so it is not ${direction}.`);
26106
26539
  return false;
26107
26540
  }
26541
+ if (ignoredEvents?.has(eventName)) {
26542
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool ignores "${tool}" on "${eventName}", so it is not ${direction}.`);
26543
+ return false;
26544
+ }
26545
+ const setFlag = hookType === "command" ? ignoredWhenFlags?.find((flag) => flags[flag] === true) : void 0;
26546
+ if (setFlag !== void 0) {
26547
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool ignores "${tool}" on a hook that sets "${setFlag}", so it is not ${direction}.`);
26548
+ return false;
26549
+ }
26108
26550
  return true;
26109
26551
  }
26110
26552
  /**
@@ -26121,6 +26563,7 @@ function emitKeywordFields({ def, hookType, eventName, fields, warn }) {
26121
26563
  spec,
26122
26564
  hookType,
26123
26565
  eventName,
26566
+ flags: def,
26124
26567
  direction: "generated",
26125
26568
  warn
26126
26569
  })) emitted[spec.tool] = spec.trueValue;
@@ -26142,6 +26585,7 @@ function importKeywordFields({ h, hookType, eventName, fields, warn }) {
26142
26585
  spec,
26143
26586
  hookType,
26144
26587
  eventName,
26588
+ flags: h,
26145
26589
  direction: "imported",
26146
26590
  warn
26147
26591
  })) continue;
@@ -28034,6 +28478,20 @@ const CLAUDE_SPEC = {
28034
28478
  tool: "continueOnBlock"
28035
28479
  }
28036
28480
  ],
28481
+ keywordPassthroughFields: [{
28482
+ canonical: "failClosed",
28483
+ tool: "onFailure",
28484
+ trueValue: "block",
28485
+ falseValue: "continue",
28486
+ hookTypes: /* @__PURE__ */ new Set(["command", "http"]),
28487
+ ignoredEvents: /* @__PURE__ */ new Set([
28488
+ "stop",
28489
+ "subagentStop",
28490
+ "taskCompleted",
28491
+ "teammateIdle"
28492
+ ]),
28493
+ ignoredWhenFlags: ["async", "asyncRewake"]
28494
+ }],
28037
28495
  arrayPassthroughFields: [{
28038
28496
  canonical: "args",
28039
28497
  tool: "args",
@@ -28756,18 +29214,20 @@ const CODEXCLI_CONVERTER_CONFIG = {
28756
29214
  canonicalToToolEventNames: CANONICAL_TO_CODEXCLI_EVENT_NAMES,
28757
29215
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
28758
29216
  projectDirVar: "",
28759
- supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
29217
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "mcp_tool"]),
28760
29218
  passthroughFields: ["name", "description"],
28761
29219
  stringPassthroughFields: [{
28762
29220
  canonical: "commandWindows",
28763
- tool: "commandWindows"
29221
+ tool: "commandWindows",
29222
+ commandOnly: true
28764
29223
  }, {
28765
29224
  canonical: "statusMessage",
28766
29225
  tool: "statusMessage"
28767
29226
  }],
28768
29227
  numberPassthroughFields: [{
28769
29228
  canonical: "additionalContextLimit",
28770
- tool: "additionalContextLimit"
29229
+ tool: "additionalContextLimit",
29230
+ commandOnly: true
28771
29231
  }],
28772
29232
  booleanPassthroughFields: [{
28773
29233
  canonical: "async",
@@ -28776,6 +29236,74 @@ const CODEXCLI_CONVERTER_CONFIG = {
28776
29236
  }]
28777
29237
  };
28778
29238
  /**
29239
+ * Whether a JSON value holds a `null` anywhere. Codex rejects an `mcp_tool`
29240
+ * hook `input` that cannot be represented as TOML (it hashes the handler as
29241
+ * TOML for trust review), and `null` is the one JSON value TOML lacks.
29242
+ */
29243
+ function containsNull(value) {
29244
+ if (value === null) return true;
29245
+ if (Array.isArray(value)) return value.some(containsNull);
29246
+ if (typeof value === "object") return Object.values(value).some(containsNull);
29247
+ return false;
29248
+ }
29249
+ const WHOLE_FILE_FAILURE = "Codex fails to load the whole hooks file on such a handler";
29250
+ /**
29251
+ * Why this generated handler must not be written, or `undefined` when Codex
29252
+ * can load and run it. Codex deserializes `timeout` as `Option<u64>` on every
29253
+ * handler type, and `server` / `tool` as required strings plus `input` as a
29254
+ * TOML-representable map on `mcp_tool`; a handler that fails that stops the
29255
+ * whole hooks file from loading rather than just that one hook. An empty
29256
+ * `server` / `tool` string still loads but can never resolve to a tool.
29257
+ * https://github.com/openai/codex/blob/main/codex-rs/config/src/hook_config.rs
29258
+ */
29259
+ function describeRejectedHandler(handler) {
29260
+ const { timeout } = handler;
29261
+ if (timeout !== void 0 && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 0)) return `its "timeout" ${JSON.stringify(timeout) ?? String(timeout)} is not a non-negative integer, and ${WHOLE_FILE_FAILURE}`;
29262
+ if (handler.type !== "mcp_tool") return;
29263
+ for (const field of ["server", "tool"]) {
29264
+ const value = handler[field];
29265
+ if (typeof value !== "string") return `it has no "${field}", and ${WHOLE_FILE_FAILURE}`;
29266
+ if (value === "") return `its "${field}" is empty, so it can never resolve to an MCP tool`;
29267
+ }
29268
+ if (containsNull(handler.input)) return `its "input" holds a null, which Codex cannot represent as TOML, and ${WHOLE_FILE_FAILURE}`;
29269
+ }
29270
+ /**
29271
+ * Drop the generated handlers Codex would refuse to load (or could never
29272
+ * run), and warn about the `mcp_tool` ones Codex loads but skips (`SessionEnd`
29273
+ * MCP hooks are skipped with a startup warning). Matcher groups left without
29274
+ * handlers are removed. https://learn.chatgpt.com/docs/hooks
29275
+ */
29276
+ function filterCodexHandlers({ hooks, logger }) {
29277
+ const result = {};
29278
+ const skippedSessionEndTools = [];
29279
+ for (const [eventName, groups] of Object.entries(hooks)) {
29280
+ const kept = [];
29281
+ for (const group of groups) {
29282
+ const entry = group;
29283
+ if (!Array.isArray(entry.hooks)) {
29284
+ kept.push(group);
29285
+ continue;
29286
+ }
29287
+ const handlers = entry.hooks.filter((handler) => {
29288
+ const reason = describeRejectedHandler(handler);
29289
+ if (reason !== void 0) {
29290
+ logger?.warn(`Skipping a "${String(handler.type)}" hook on Codex CLI "${eventName}": ${reason}.`);
29291
+ return false;
29292
+ }
29293
+ if (eventName === "SessionEnd" && handler.type === "mcp_tool") skippedSessionEndTools.push(`${String(handler.server)}/${String(handler.tool)}`);
29294
+ return true;
29295
+ });
29296
+ if (handlers.length > 0) kept.push({
29297
+ ...entry,
29298
+ hooks: handlers
29299
+ });
29300
+ }
29301
+ if (kept.length > 0) result[eventName] = kept;
29302
+ }
29303
+ if (skippedSessionEndTools.length > 0) logger?.warn(`Codex CLI skips "mcp_tool" hooks on "SessionEnd" with a startup warning, so these are written but will not run: ${skippedSessionEndTools.join(", ")}.`);
29304
+ return result;
29305
+ }
29306
+ /**
28779
29307
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
28780
29308
  * Reads the existing file (if any), parses TOML, and returns the content without writing to
28781
29309
  * disk. The caller is responsible for writing via the normal write phase.
@@ -28868,10 +29396,13 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
28868
29396
  const config = rulesyncHooks.getJson();
28869
29397
  const merged = mergeGeneratedHookLists({
28870
29398
  existingContent,
28871
- generatedHooks: canonicalToToolHooks({
28872
- config,
28873
- toolOverrideHooks: config.codexcli?.hooks,
28874
- converterConfig: CODEXCLI_CONVERTER_CONFIG,
29399
+ generatedHooks: filterCodexHandlers({
29400
+ hooks: canonicalToToolHooks({
29401
+ config,
29402
+ toolOverrideHooks: config.codexcli?.hooks,
29403
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
29404
+ logger
29405
+ }),
28875
29406
  logger
28876
29407
  }),
28877
29408
  shape: "matcher-groups",
@@ -32050,60 +32581,6 @@ var KiloHooks = class KiloHooks extends ToolHooks {
32050
32581
  }
32051
32582
  };
32052
32583
  //#endregion
32053
- //#region src/constants/kimi-code-paths.ts
32054
- const KIMI_CODE_DIR = ".kimi-code";
32055
- const KIMI_CODE_RULE_FILE_NAME = "AGENTS.md";
32056
- const KIMI_CODE_MCP_FILE_NAME = "mcp.json";
32057
- const KIMI_CODE_CONFIG_FILE_NAME = "config.toml";
32058
- const KIMI_CODE_SKILLS_DIR_NAME = "skills";
32059
- const KIMI_CODE_AGENTS_DIR_NAME = "agents";
32060
- const KIMI_CODE_SKILLS_DIR_PATH = (0, node_path.join)(KIMI_CODE_DIR, KIMI_CODE_SKILLS_DIR_NAME);
32061
- (0, node_path.join)(KIMI_CODE_DIR, KIMI_CODE_AGENTS_DIR_NAME);
32062
- const KIMI_CODE_SHARED_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
32063
- const KIMI_CODE_SHARED_AGENTS_DIR_PATH = (0, node_path.join)(".agents", "agents");
32064
- //#endregion
32065
- //#region src/utils/kimi-code.ts
32066
- function getKimiCodeHome() {
32067
- const configuredHome = process.env.KIMI_CODE_HOME?.trim();
32068
- return configuredHome ? (0, node_path.resolve)(configuredHome) : void 0;
32069
- }
32070
- function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
32071
- return global && getKimiCodeHome() ? relativeDirPath : (0, node_path.join)(KIMI_CODE_DIR, relativeDirPath);
32072
- }
32073
- /**
32074
- * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
32075
- * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
32076
- * Declared unconditionally so the derived shared-file keys — and the drift
32077
- * guards checked against them — do not depend on the ambient environment.
32078
- */
32079
- function getKimiCodeSharedConfigWritePaths() {
32080
- return [{
32081
- relativeDirPath: KIMI_CODE_DIR,
32082
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32083
- }, {
32084
- relativeDirPath: ".",
32085
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32086
- }];
32087
- }
32088
- /**
32089
- * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
32090
- * Both spellings carry the same declaration, but passing the key of the file
32091
- * being written keeps the write path and the drift guards on the same entry.
32092
- */
32093
- function getKimiCodeConfigSharedFileKey({ global }) {
32094
- return sharedConfigFileKey({
32095
- relativeDirPath: getKimiCodeRelativeDirPath({ global }),
32096
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32097
- });
32098
- }
32099
- function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
32100
- return getToolRulesyncOutputRoot({
32101
- nativeOutputRoot,
32102
- global,
32103
- toolHome: getKimiCodeHome
32104
- });
32105
- }
32106
- //#endregion
32107
32584
  //#region src/features/hooks/kimi-code-hooks.ts
32108
32585
  function runFromTrustedDirectory({ command, trustedDirectory }) {
32109
32586
  if (process.platform === "win32") return `set "RULESYNC_KIMI_HOOK_CWD=1" && cd /d "${trustedDirectory.replaceAll("%", "%%").replaceAll("\"", "\"\"")}" && ${command}`;
@@ -35560,7 +36037,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
35560
36037
  supportsImport: true
35561
36038
  },
35562
36039
  supportedEvents: CODEXCLI_HOOK_EVENTS,
35563
- supportedHookTypes: ["command"],
36040
+ supportedHookTypes: ["command", "mcp_tool"],
35564
36041
  supportsMatcher: true
35565
36042
  }],
35566
36043
  ["commandcode", {
@@ -37822,6 +38299,23 @@ function toReasonixTable(value) {
37822
38299
  if (!isPlainObject$1(value)) return {};
37823
38300
  return { ...value };
37824
38301
  }
38302
+ /**
38303
+ * The tool name a Reasonix `[permissions]` rule must use to reach file reads.
38304
+ * Reasonix matches a rule's tool exactly against its registered tools
38305
+ * (`read_file`, `write_file`, `grep`, ...); only `Bash`, `Edit`, `Browser` and
38306
+ * `Computer` are Claude-Code-style aliases (`canonicalRuleTool` in
38307
+ * `internal/safety/permission/permission.go`), so a `Read(...)` rule matches
38308
+ * nothing. Shared by the `ignore` and `permissions` adapters, which both write
38309
+ * read denies into the same table.
38310
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.41.0/internal/safety/permission/permission.go
38311
+ */
38312
+ const REASONIX_READ_TOOL_NAME = "read_file";
38313
+ /**
38314
+ * The spelling rulesync wrote for read rules before it followed the Reasonix
38315
+ * tool vocabulary. Still accepted on import, and an entry spelled this way is
38316
+ * treated as rulesync-owned on generate so the dormant copy is replaced.
38317
+ */
38318
+ const REASONIX_LEGACY_READ_TOOL_NAME = "Read";
37825
38319
  //#endregion
37826
38320
  //#region src/features/ignore/reasonix-ignore.ts
37827
38321
  const permissionsTableOf = (document) => toReasonixTable(document.permissions);
@@ -37840,10 +38334,15 @@ const asClaudeStyleSettings = (document) => {
37840
38334
  ...table,
37841
38335
  allow: toReasonixStringArray(table.allow),
37842
38336
  ask: toReasonixStringArray(table.ask),
37843
- deny: toReasonixStringArray(table.deny)
38337
+ deny: toReasonixStringArray(table.deny).filter((entry) => !isReadDenyEntry(entry, REASONIX_LEGACY_READ_TOOL_NAME))
37844
38338
  }
37845
38339
  };
37846
38340
  };
38341
+ /** The pattern of a `read_file(...)` (or legacy `Read(...)`) deny, else null. */
38342
+ const readDenyPatternOf = (entry) => {
38343
+ for (const toolName of [REASONIX_READ_TOOL_NAME, REASONIX_LEGACY_READ_TOOL_NAME]) if (isReadDenyEntry(entry, toolName)) return entry.slice(toolName.length + 1, -1);
38344
+ return null;
38345
+ };
37847
38346
  /**
37848
38347
  * Drop a `[permissions]` table that ended up with nothing in it, so an empty
37849
38348
  * `.rulesyncignore` does not add a bare table header to a file that never had
@@ -37856,13 +38355,16 @@ const withoutEmptyPermissions = (settings) => {
37856
38355
  return document;
37857
38356
  };
37858
38357
  /**
37859
- * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
38358
+ * Writes `.rulesyncignore` patterns as `read_file(<pattern>)` entries in the
37860
38359
  * `[permissions] deny` table of `reasonix.toml` (project) /
37861
38360
  * `~/.reasonix/config.toml` (global).
37862
38361
  *
37863
38362
  * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
37864
38363
  * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
37865
38364
  * while `forbid_read` is documented as absolute paths with no glob support.
38365
+ * The tool is spelled `read_file` because Reasonix matches rule tools against
38366
+ * its registered tool names (see {@link REASONIX_READ_TOOL_NAME}); a legacy
38367
+ * `Read(...)` deny is still read back on import.
37866
38368
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
37867
38369
  */
37868
38370
  var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
@@ -37889,7 +38391,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
37889
38391
  return false;
37890
38392
  }
37891
38393
  toRulesyncIgnore() {
37892
- const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
38394
+ const rulesyncPatterns = (0, es_toolkit.uniq)(this.patterns.map((entry) => readDenyPatternOf(entry)).filter((pattern) => pattern !== null && pattern.length > 0));
37893
38395
  return new RulesyncIgnore({
37894
38396
  outputRoot: this.outputRoot,
37895
38397
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
@@ -37898,7 +38400,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
37898
38400
  });
37899
38401
  }
37900
38402
  static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
37901
- const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
38403
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern, REASONIX_READ_TOOL_NAME));
37902
38404
  const paths = this.getSettablePaths({ global });
37903
38405
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
37904
38406
  const existingDocument = parseSharedConfig({
@@ -37908,7 +38410,8 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
37908
38410
  });
37909
38411
  const document = withoutEmptyPermissions(applyIgnoreReadDenies({
37910
38412
  settings: asClaudeStyleSettings(existingDocument),
37911
- readDenies
38413
+ readDenies,
38414
+ readToolName: REASONIX_READ_TOOL_NAME
37912
38415
  }));
37913
38416
  return new ReasonixIgnore({
37914
38417
  outputRoot,
@@ -38053,6 +38556,61 @@ var TabnineIgnore = class TabnineIgnore extends ToolIgnore {
38053
38556
  }
38054
38557
  };
38055
38558
  //#endregion
38559
+ //#region src/features/ignore/trae-ignore.ts
38560
+ /**
38561
+ * Trae ignore file implementation.
38562
+ *
38563
+ * Trae reads `.trae/.ignore` (gitignore syntax) as a supplement to
38564
+ * `.gitignore` when it indexes the codebase; ignored files are also left out
38565
+ * of `#Workspace` / `#Folder` context. There is no user-level ignore file, so
38566
+ * only project scope is supported.
38567
+ *
38568
+ * @see https://docs.trae.ai/ide/ignore-files?_lang=en
38569
+ */
38570
+ var TraeIgnore = class TraeIgnore extends ToolIgnore {
38571
+ static getSettablePaths(_params = {}) {
38572
+ return {
38573
+ relativeDirPath: TRAE_DIR,
38574
+ relativeFilePath: TRAE_IGNORE_FILE_NAME
38575
+ };
38576
+ }
38577
+ toRulesyncIgnore() {
38578
+ return this.toRulesyncIgnoreDefault();
38579
+ }
38580
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
38581
+ const paths = this.getSettablePaths({ global });
38582
+ return new TraeIgnore({
38583
+ outputRoot,
38584
+ relativeDirPath: paths.relativeDirPath,
38585
+ relativeFilePath: paths.relativeFilePath,
38586
+ fileContent: rulesyncIgnore.getFileContent(),
38587
+ global
38588
+ });
38589
+ }
38590
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
38591
+ const { relativeDirPath, relativeFilePath } = this.getSettablePaths({ global });
38592
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
38593
+ return new TraeIgnore({
38594
+ outputRoot,
38595
+ relativeDirPath,
38596
+ relativeFilePath,
38597
+ fileContent,
38598
+ validate,
38599
+ global
38600
+ });
38601
+ }
38602
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
38603
+ return new TraeIgnore({
38604
+ outputRoot,
38605
+ relativeDirPath,
38606
+ relativeFilePath,
38607
+ fileContent: "",
38608
+ validate: false,
38609
+ global
38610
+ });
38611
+ }
38612
+ };
38613
+ //#endregion
38056
38614
  //#region src/features/ignore/vibe-ignore.ts
38057
38615
  var VibeIgnore = class VibeIgnore extends ToolIgnore {
38058
38616
  static getSettablePaths() {
@@ -38281,6 +38839,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
38281
38839
  ["zoocode", { class: RooIgnore }],
38282
38840
  ["devin", { class: DevinIgnore }],
38283
38841
  ["tabnine", { class: TabnineIgnore }],
38842
+ ["trae", { class: TraeIgnore }],
38284
38843
  ["vibe", { class: VibeIgnore }],
38285
38844
  ["warp", { class: WarpIgnore }],
38286
38845
  ["zed", { class: ZedIgnore }]
@@ -43633,12 +44192,14 @@ function toGitlabduoTimeout({ serverName, server, logger }) {
43633
44192
  /**
43634
44193
  * Read GitLab Duo's `timeoutMs` back into the canonical `timeout`
43635
44194
  * (milliseconds). A server that already carries a `timeout` is left as is, so
43636
- * both keys round-trip and `timeoutMs` keeps winning on the next generate.
44195
+ * both keys round-trip and `timeoutMs` keeps winning on the next generate. A
44196
+ * non-number `timeoutMs` is not copied, since the canonical `timeout` must be a
44197
+ * number; it is kept verbatim as a passthrough key instead.
43637
44198
  */
43638
44199
  function fromGitlabduoTimeout(server) {
43639
44200
  if (typeof server !== "object" || server === null || Array.isArray(server)) return server;
43640
44201
  const { timeoutMs, ...rest } = server;
43641
- if (timeoutMs === void 0 || rest.timeout !== void 0) return server;
44202
+ if (typeof timeoutMs !== "number" || rest.timeout !== void 0) return server;
43642
44203
  return {
43643
44204
  ...rest,
43644
44205
  timeout: timeoutMs
@@ -45353,7 +45914,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
45353
45914
  feature: "mcp",
45354
45915
  existingContent: fileContent ?? "",
45355
45916
  patch: {
45356
- mcp: convertedMcp,
45917
+ mcp: undefinedWhenEmpty(convertedMcp),
45357
45918
  ...buildKiloMcpPermissionPatch({
45358
45919
  existing: existingDocument,
45359
45920
  managedServerNames: (0, es_toolkit.uniq)([...Object.keys(mcpServers), ...Object.keys(isRecord$1(existingDocument.mcp) ? existingDocument.mcp : {})]),
@@ -46213,7 +46774,7 @@ var OpencodeMcp = class extends ToolMcp {
46213
46774
  feature: "mcp",
46214
46775
  existingContent: fileContent ?? "",
46215
46776
  patch: {
46216
- mcp: convertedMcp,
46777
+ mcp: undefinedWhenEmpty(convertedMcp),
46217
46778
  tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
46218
46779
  },
46219
46780
  filePath: (0, node_path.join)(jsonDir, relativeFilePath),
@@ -46561,9 +47122,12 @@ function convertFromMusecodeFormat(musecodeMcp) {
46561
47122
  /**
46562
47123
  * Meta Muse Code MCP servers.
46563
47124
  *
46564
- * Muse Code reads MCP servers only from the `mcp_servers` (or `mcpServers`)
46565
- * block of the GLOBAL user settings file `~/.config/muse/settings.json`; no
46566
- * project-scoped MCP location is documented. The settings file must carry
47125
+ * rulesync writes Muse Code MCP servers only to the `mcp_servers` (or
47126
+ * `mcpServers`) block of the GLOBAL user settings file
47127
+ * `~/.config/muse/settings.json`, the location extending.md documents. Muse
47128
+ * Code 1.4.0+ also loads a trusted project `.mcp.json` (changelog only), which
47129
+ * is not emitted here because the `claudecode` target already writes that
47130
+ * file. The settings file must carry
46567
47131
  * `"schema_version": 1` — a file that omits that key fails every command at
46568
47132
  * startup with `malformed settings file` — so the key is bootstrapped when the
46569
47133
  * file is created and preserved when it already exists. Other settings keys are
@@ -46571,6 +47135,7 @@ function convertFromMusecodeFormat(musecodeMcp) {
46571
47135
  *
46572
47136
  * @see https://dev.meta.ai/docs/muse-code/configuration.md
46573
47137
  * @see https://dev.meta.ai/docs/muse-code/extending.md
47138
+ * @see https://dev.meta.ai/docs/muse-code/changelog
46574
47139
  */
46575
47140
  var MusecodeMcp = class MusecodeMcp extends ToolMcp {
46576
47141
  json;
@@ -47111,8 +47676,8 @@ function convertFromPiFormat(mcpServers) {
47111
47676
  * existing file (`autoEnableCodemode`, ...) are preserved. Pi expands `${VAR}`
47112
47677
  * in `env` and `headers` values itself, so those are written unchanged.
47113
47678
  *
47114
- * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md
47115
- * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/mcp-servers.ts
47679
+ * @see https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/docs/mcp.md
47680
+ * @see https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/src/core/mcp-servers.ts
47116
47681
  */
47117
47682
  var PiMcp = class PiMcp extends ToolMcp {
47118
47683
  json;
@@ -47803,7 +48368,7 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
47803
48368
  feature: "mcp",
47804
48369
  existingContent,
47805
48370
  patch: {
47806
- mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers()),
48371
+ mcpServers: undefinedWhenEmpty(convertToQwencodeFormat(rulesyncMcp.getMcpServers())),
47807
48372
  ...mcpPatch !== void 0 && { mcp: mcpPatch }
47808
48373
  },
47809
48374
  filePath
@@ -49226,21 +49791,6 @@ function deriveTransportAllowlist(servers) {
49226
49791
  return allowlist;
49227
49792
  }
49228
49793
  //#endregion
49229
- //#region src/constants/trae-paths.ts
49230
- /**
49231
- * Trae (ByteDance's AI IDE, "TraeCode" in its current docs) keeps project
49232
- * configuration under `.trae/` and user configuration under `~/.trae/`.
49233
- *
49234
- * @see https://docs.trae.ai/ide/rules?_lang=en
49235
- * @see https://docs.trae.ai/ide/skills?_lang=en
49236
- * @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
49237
- */
49238
- const TRAE_DIR = ".trae";
49239
- /** Project MCP servers (`mcpServers` object), project scope only. */
49240
- const TRAE_MCP_FILE_NAME = "mcp.json";
49241
- /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
49242
- const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
49243
- //#endregion
49244
49794
  //#region src/features/mcp/trae-mcp.ts
49245
49795
  /** A `${...}` reference Trae does not expand: anything but `${workspaceFolder}`. */
49246
49796
  const UNEXPANDED_VARIABLE_REGEX = /\$\{(?!workspaceFolder\})[^}]+\}/;
@@ -51850,6 +52400,11 @@ function parsedGlobsIntersect(left, right, budget) {
51850
52400
  * @see https://antigravity.google/docs/permissions?tab=cli
51851
52401
  */
51852
52402
  const REGEX_PREFIX = "regex:";
52403
+ /**
52404
+ * The Antigravity actions whose target is a command: `command`, and
52405
+ * `unsandboxed`, which takes the same word-by-word prefix or `regex:` target.
52406
+ */
52407
+ const ANTIGRAVITY_COMMAND_ACTIONS = /* @__PURE__ */ new Set(["command", "unsandboxed"]);
51853
52408
  const TRAILING_ANCHOR = /(?<!\\)\$$/;
51854
52409
  const PLAIN_TARGET_GLOB_CHARACTERS = /[*?[{\\]/;
51855
52410
  const REGEX_METACHARACTERS$1 = /[\\^$.|?*+()[\]{}]/;
@@ -52227,8 +52782,8 @@ function toAntigravityCommandLiteralPrefix(pattern) {
52227
52782
  }
52228
52783
  /**
52229
52784
  * Read one regex word back as a glob, or `undefined` when it uses anything a
52230
- * glob cannot say. Antigravity anchors each word anyway, so `^` and `$` are
52231
- * optional. Only escaped characters and `.*` translate; anything else keeps
52785
+ * glob cannot say. Antigravity anchors each word anyway (`^(?:pattern)$`), so
52786
+ * `^` and `$` are optional. Only escaped characters and `.*` translate; anything else keeps
52232
52787
  * the rule a regex.
52233
52788
  */
52234
52789
  function regexWordToGlob(word) {
@@ -52254,8 +52809,10 @@ function regexWordToGlob(word) {
52254
52809
  }
52255
52810
  /** Read a per-word regex back as a glob, or `undefined` when it has no glob spelling. */
52256
52811
  function regexToGlob(source) {
52812
+ const words = splitWords(source);
52813
+ if (words.length === 0) return;
52257
52814
  const globs = [];
52258
- for (const word of splitWords(source)) {
52815
+ for (const word of words) {
52259
52816
  const glob = regexWordToGlob(word);
52260
52817
  if (glob === void 0) return;
52261
52818
  globs.push(glob);
@@ -52280,36 +52837,23 @@ function fromAntigravityCommandTarget(target) {
52280
52837
  return `${target} *`;
52281
52838
  }
52282
52839
  /**
52283
- * Whether a regex word means the same anchored or not: each end is either
52284
- * anchored (`^`, an unescaped `$`) or open-ended (`.*`).
52285
- */
52286
- function isAnchoredWord(word) {
52287
- const startAnchored = word.startsWith("^") || word.startsWith(".*");
52288
- const endAnchored = TRAILING_ANCHOR.test(word) || word.endsWith(".*");
52289
- return startAnchored && endAnchored;
52290
- }
52291
- /**
52292
52840
  * Explain what importing an Antigravity `command` target may lose, or return
52293
52841
  * `undefined` when the import is faithful.
52294
52842
  *
52295
- * - A `regex:` target with a word that is not anchored is imported as a glob
52296
- * that assumes anchoring. If Antigravity matches such a word unanchored,
52297
- * the imported rule is narrower (`regex:rm` also matches `rmdir`).
52298
- * - A plain target holding characters that are special in a glob is most
52299
- * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
52300
- * Antigravity matches it literally, so it imports as a regex that matches a
52301
- * literal `*` word and is inert in the other tools it is exported to.
52843
+ * A plain target holding characters that are special in a glob is most
52844
+ * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
52845
+ * Antigravity matches it literally, so it imports as a regex that matches a
52846
+ * literal `*` word and is inert in the other tools it is exported to.
52847
+ *
52848
+ * A `regex:` word without `^...$` (`regex:rm`) is read as anchored, which is
52849
+ * faithful: Antigravity documents that each token is evaluated as the
52850
+ * anchored regex `^(?:pattern)$`.
52851
+ *
52852
+ * @see https://antigravity.google/docs/permissions
52302
52853
  */
52303
52854
  function describeAntigravityCommandImportLoss(target) {
52304
- if (target.startsWith(REGEX_PREFIX)) {
52305
- const source = target.slice(6);
52306
- if (splitWords(source).some((word) => !isAnchoredWord(word)) && regexToGlob(source) !== void 0) return {
52307
- reason: "it has a regex word without `^...$` anchors and is imported as a glob that assumes Antigravity anchors every word, so the imported rule may be narrower than the original",
52308
- denyConsequence: "The imported deny may block less than it does in Antigravity."
52309
- };
52310
- return;
52311
- }
52312
- if (target !== "*" && PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
52855
+ if (target.startsWith(REGEX_PREFIX) || target === "*") return;
52856
+ if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
52313
52857
  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",
52314
52858
  denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
52315
52859
  };
@@ -52341,17 +52885,20 @@ function importAntigravityCommandTarget({ target, entry, decision, logger, toolL
52341
52885
  * every file.
52342
52886
  * - `read_url` takes a domain and covers its subdomains. The URL path is
52343
52887
  * ignored, and `*` covers every domain.
52344
- * - `command` takes literal words or a per-word `regex:`, matched word by
52345
- * word (see `antigravity-command-patterns.ts`).
52888
+ * - `execute_url` takes a domain, like `read_url`.
52889
+ * - `command` and `unsandboxed` take literal words or a per-word `regex:`,
52890
+ * matched word by word (see `antigravity-command-patterns.ts`).
52346
52891
  *
52347
52892
  * A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
52348
52893
  * matches nothing, so a deny written that way blocks nothing. A pattern with
52349
- * no Antigravity spelling is skipped with a warning instead, except a command
52350
- * deny with leading literal words, which is widened to them.
52894
+ * no Antigravity spelling is skipped with a warning instead, except a deny
52895
+ * with a literal lead (the first words of a command, the leading directories
52896
+ * of a path), which is widened to it.
52351
52897
  *
52352
52898
  * @see https://antigravity.google/docs/permissions?tab=cli
52353
52899
  */
52354
52900
  const PATH_ACTIONS = /* @__PURE__ */ new Set(["read_file", "write_file"]);
52901
+ const URL_ACTIONS = /* @__PURE__ */ new Set(["read_url", "execute_url"]);
52355
52902
  const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
52356
52903
  "*",
52357
52904
  "**",
@@ -52363,7 +52910,8 @@ const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
52363
52910
  const DENY_HINTS = {
52364
52911
  read_file: "deny a directory or file path instead",
52365
52912
  write_file: "deny a directory or file path instead",
52366
- command: "deny the first words of the command instead"
52913
+ command: "deny the first words of the command instead",
52914
+ unsandboxed: "deny the first words of the command instead"
52367
52915
  };
52368
52916
  function hasGlob(pattern) {
52369
52917
  return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
@@ -52414,37 +52962,88 @@ function toUrlTarget(pattern) {
52414
52962
  };
52415
52963
  }
52416
52964
  function toTarget(action, pattern) {
52417
- if (action === "command") return toAntigravityCommandTarget(pattern);
52965
+ if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandTarget(pattern);
52418
52966
  if (PATH_ACTIONS.has(action)) return toPathTarget(pattern);
52419
- if (action === "read_url") return toUrlTarget(pattern);
52967
+ if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
52420
52968
  return { target: pattern };
52421
52969
  }
52422
52970
  /**
52971
+ * The leading directories of a path glob (`src` for `src/*.ts`), or
52972
+ * `undefined` when it has none. A directory target covers everything inside
52973
+ * it, so it matches at least as much as the glob. The filesystem root alone
52974
+ * (`/*`) is not a lead: a deny of it would block every file. Neither is a lead
52975
+ * with `..` in it.
52976
+ */
52977
+ function toPathLiteralPrefix(pattern) {
52978
+ const path = pattern.startsWith("./") ? pattern.slice(2) : pattern;
52979
+ const prefix = [];
52980
+ for (const segment of path.split("/").slice(0, -1)) {
52981
+ if (hasGlob(segment) || segment.includes("{") || segment.includes("}")) break;
52982
+ prefix.push(segment);
52983
+ }
52984
+ if (prefix.includes("..") || prefix.every((segment) => segment === "" || segment === ".")) return;
52985
+ return prefix.join("/");
52986
+ }
52987
+ /** The widened target of a deny that has no exact Antigravity spelling. */
52988
+ function toWidenedDenyTarget(action, pattern) {
52989
+ if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
52990
+ if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
52991
+ }
52992
+ /**
52993
+ * Warn about a path target that is a home or variable reference. Antigravity
52994
+ * documents absolute and workspace-relative paths only, so such a reference
52995
+ * may be matched literally and never apply. The entry is still written, since
52996
+ * dropping a deny would lose it where Antigravity does expand the reference.
52997
+ */
52998
+ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLabel }) {
52999
+ if (!PATH_ACTIONS.has(action) || !HOME_OR_VARIABLE_PATH.test(target)) return;
53000
+ const consequence = decision === "deny" ? "this deny may block nothing" : decision === "ask" ? `${toolLabel} may fall back to its default, so it may run without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} may fall back to its default`;
53001
+ logger.warn(`${toolLabel} permissions: ${rule}: \`~\` and environment variables may not be expanded in Antigravity path targets, so ${action}(${target}) may match nothing and ${consequence}; use an absolute path instead.`);
53002
+ }
53003
+ /**
52423
53004
  * Build the entry for one canonical rule, or return `undefined` when
52424
- * Antigravity cannot express it. A command deny is widened to its leading
52425
- * literal words rather than skipped: Antigravity applies deny over ask and
52426
- * allow, so an overlapping allow that is translated (`git push *`) cannot let
52427
- * through what the deny (`git push * --force`) was meant to block. A skipped
52428
- * deny is warned about loudly, because nothing enforces it.
53005
+ * Antigravity cannot express it. A deny is widened to its literal lead rather
53006
+ * than skipped (`git push * --force` → `command(git push)`, `src/*.ts` →
53007
+ * `read_file(src)`): Antigravity applies deny over ask and allow, so an
53008
+ * overlapping allow that is translated (`git push *`) cannot let through what
53009
+ * the deny was meant to block. A skipped deny is warned about loudly, because
53010
+ * nothing enforces it, and a widened one is warned about too, because it
53011
+ * blocks more than it says.
52429
53012
  */
52430
53013
  function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
52431
53014
  const result = pattern === "*" ? { target: "*" } : toTarget(action, pattern);
52432
53015
  const rule = `${category}: { "${pattern}": "${decision}" }`;
52433
53016
  if ("skipReason" in result) {
52434
- const prefix = action === "command" && decision === "deny" ? toAntigravityCommandLiteralPrefix(pattern) : void 0;
53017
+ const prefix = decision === "deny" ? toWidenedDenyTarget(action, pattern) : void 0;
52435
53018
  if (prefix !== void 0) {
52436
- logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks every command that starts with \`${prefix}\`, overriding any allow or ask for them.`);
53019
+ const scope = PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``;
53020
+ logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
53021
+ warnHomeOrVariablePath({
53022
+ action,
53023
+ target: prefix,
53024
+ rule,
53025
+ decision,
53026
+ logger,
53027
+ toolLabel
53028
+ });
52437
53029
  return `${action}(${prefix})`;
52438
53030
  }
52439
53031
  const consequence = decision === "deny" ? `This deny is NOT enforced in ${toolLabel}; ${DENY_HINTS[action] ?? "write it in a form Antigravity can match"}` : decision === "ask" ? `${toolLabel} falls back to its default for it, so it runs without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} falls back to its default for it`;
52440
53032
  logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
52441
53033
  return;
52442
53034
  }
52443
- if (result.note !== void 0 && decision !== "deny") logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}.`);
52444
- if (PATH_ACTIONS.has(action) && HOME_OR_VARIABLE_PATH.test(result.target)) {
52445
- const consequence = decision === "deny" ? "this deny may block nothing" : `${toolLabel} may fall back to its default`;
52446
- logger.warn(`${toolLabel} permissions: ${rule}: \`~\` and environment variables may not be expanded in Antigravity path targets, so ${action}(${result.target}) may match nothing and ${consequence}; use an absolute path instead.`);
53035
+ if (result.note !== void 0 && (decision !== "deny" || URL_ACTIONS.has(action))) {
53036
+ const consequence = decision === "deny" ? ", so the deny also overrides any allow or ask there" : "";
53037
+ logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}${consequence}.`);
52447
53038
  }
53039
+ warnHomeOrVariablePath({
53040
+ action,
53041
+ target: result.target,
53042
+ rule,
53043
+ decision,
53044
+ logger,
53045
+ toolLabel
53046
+ });
52448
53047
  return result.target === "*" ? action : `${action}(${result.target})`;
52449
53048
  }
52450
53049
  //#endregion
@@ -52778,7 +53377,8 @@ function pickDocumentedValue({ key, value, documented }) {
52778
53377
  * Deliberate mapping from rulesync canonical permission categories to the
52779
53378
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
52780
53379
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
52781
- * `unsandboxed` which have no canonical equivalent and pass through). `edit` and
53380
+ * `unsandboxed` which have no canonical equivalent and keep their names; their
53381
+ * targets are translated like `read_url` / `command` ones). `edit` and
52782
53382
  * `write` both map to `write_file`, and `webfetch` / `websearch` both map to
52783
53383
  * `read_url`; on import those collapse to the canonical `write` / `webfetch`
52784
53384
  * form (a documented, lossy normalization). This shares the action vocabulary of
@@ -53025,7 +53625,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
53025
53625
  for (const entry of entries) {
53026
53626
  const { toolName, pattern: target } = parsePermissionEntry$1(entry);
53027
53627
  const canonical = toCanonicalToolName$9(toolName);
53028
- const pattern = toolName === "command" ? importAntigravityCommandTarget({
53628
+ const pattern = ANTIGRAVITY_COMMAND_ACTIONS.has(toolName) ? importAntigravityCommandTarget({
53029
53629
  target,
53030
53630
  entry,
53031
53631
  decision: action,
@@ -53047,7 +53647,8 @@ function convertAntigravityCliToRulesyncPermissions(params) {
53047
53647
  * Deliberate mapping from rulesync canonical permission categories to the
53048
53648
  * Antigravity IDE action vocabulary (`read_file`, `write_file`, `read_url`,
53049
53649
  * `command`, `mcp`, plus `execute_url` / `unsandboxed` which have no canonical
53050
- * equivalent and pass through). `edit` and `write` both map to `write_file`, and
53650
+ * equivalent and keep their names; their targets are translated like
53651
+ * `read_url` / `command` ones). `edit` and `write` both map to `write_file`, and
53051
53652
  * `webfetch` / `websearch` both map to `read_url`; on import those collapse to
53052
53653
  * the canonical `write` / `webfetch` form (a documented, lossy normalization).
53053
53654
  */
@@ -53249,7 +53850,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
53249
53850
  for (const entry of entries) {
53250
53851
  const { action: ideAction, pattern: target } = parsePermissionEntry(entry);
53251
53852
  const canonical = toCanonicalCategory$2(ideAction);
53252
- const pattern = ideAction === "command" ? importAntigravityCommandTarget({
53853
+ const pattern = ANTIGRAVITY_COMMAND_ACTIONS.has(ideAction) ? importAntigravityCommandTarget({
53253
53854
  target,
53254
53855
  entry,
53255
53856
  decision: action,
@@ -55179,6 +55780,7 @@ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
55179
55780
  "dialogExpiry",
55180
55781
  "feedbackDrafts",
55181
55782
  "footerLinksRegexes",
55783
+ "forceLoginGatewayUrl",
55182
55784
  "modelPicker",
55183
55785
  "pluginConfigs",
55184
55786
  "prependPlugins",
@@ -55192,6 +55794,39 @@ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
55192
55794
  "vimInsertModeRemaps"
55193
55795
  ]);
55194
55796
  /**
55797
+ * Nested settings paths under an otherwise project-honored top-level key that
55798
+ * Claude Code reads only from user settings — the nested counterpart of
55799
+ * {@link CLAUDECODE_USER_SCOPE_ONLY_KEYS}. `worktree` is `Any file`, but
55800
+ * `worktree.location` (where Desktop SSH sessions create worktrees on the
55801
+ * remote machine) is documented with a `User` scope, so it is dropped from a
55802
+ * project `.claude/settings.json` and written only under `--global`, while its
55803
+ * siblings are kept.
55804
+ *
55805
+ * @see https://code.claude.com/docs/en/settings-reference#worktree-location
55806
+ */
55807
+ const CLAUDECODE_USER_SCOPE_ONLY_NESTED_PATHS = [["worktree", "location"]];
55808
+ /**
55809
+ * Drops {@link CLAUDECODE_USER_SCOPE_ONLY_NESTED_PATHS} from the top-level
55810
+ * passthrough at project scope, in place, warning once per dropped path. The
55811
+ * authored subtree is copied before the deletion so the caller's config is not
55812
+ * mutated, and a parent the removal emptied is dropped with it.
55813
+ */
55814
+ function stripUserScopeOnlyNestedPaths({ overrides, global, relativeFilePath, logger }) {
55815
+ if (global) return;
55816
+ for (const path of CLAUDECODE_USER_SCOPE_ONLY_NESTED_PATHS) {
55817
+ const [topLevelKey] = path;
55818
+ if (topLevelKey === void 0 || !isRecord$1(overrides[topLevelKey])) continue;
55819
+ const copy = { [topLevelKey]: structuredClone(overrides[topLevelKey]) };
55820
+ if (!deleteSandboxPath({
55821
+ target: copy,
55822
+ path
55823
+ })) continue;
55824
+ if (copy[topLevelKey] === void 0) delete overrides[topLevelKey];
55825
+ else overrides[topLevelKey] = copy[topLevelKey];
55826
+ logger?.warn(`Claude Code permissions: '${path.map((segment) => displayKey(segment)).join(".")}' is not honored in the project-scoped ${relativeFilePath}, so it is not written there — Claude Code reads it from user settings only. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
55827
+ }
55828
+ }
55829
+ /**
55195
55830
  * Top-level settings keys neither file rulesync writes can honor, with the file
55196
55831
  * that does. `Managed` keys are read only from the settings file an organization
55197
55832
  * deploys, and `Global config` keys only from `~/.claude.json` — rulesync writes
@@ -55229,7 +55864,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
55229
55864
  disableMobileSimulatorTools: "managed settings",
55230
55865
  disableSideloadFlags: "managed settings",
55231
55866
  externalEditorContext: "~/.claude.json",
55232
- forceLoginGatewayUrl: "managed settings",
55233
55867
  forceRemoteSettingsRefresh: "managed settings",
55234
55868
  gatewayInternalNetworks: "managed settings",
55235
55869
  leftArrowOpensAgents: "~/.claude.json",
@@ -55388,8 +56022,9 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
55388
56022
  /**
55389
56023
  * `env` variables a project or local settings file cannot set, because a
55390
56024
  * checked-out repository should not control them: where Claude Code stores its
55391
- * own files, what session content it exports, where telemetry goes, and how it
55392
- * starts or syncs. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
56025
+ * own files, what session content it exports, where telemetry goes, how it
56026
+ * starts or syncs, the timers on an unanswered dialog, and whether it processes
56027
+ * attachments or expands `@` file mentions. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
55393
56028
  *
55394
56029
  * Only the names the settings reference lists are covered; its directory and
55395
56030
  * Windows groups are introduced with "such as", so an unlisted member of those
@@ -55400,6 +56035,10 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
55400
56035
  */
55401
56036
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
55402
56037
  "BETA_TRACING_ENDPOINT",
56038
+ "CLAUDE_AFK_COUNTDOWN_MS",
56039
+ "CLAUDE_AFK_TIMEOUT_MS",
56040
+ "CLAUDE_CODE_DISABLE_ATTACHMENTS",
56041
+ "CLAUDE_CODE_DISABLE_FILE_MENTIONS",
55403
56042
  "CLAUDE_CODE_ENABLE_TELEMETRY",
55404
56043
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
55405
56044
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
@@ -55408,6 +56047,7 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
55408
56047
  "CLAUDE_CODE_SYNC_PLUGINS",
55409
56048
  "CLAUDE_CODE_SYNC_SKILLS",
55410
56049
  "CLAUDE_CODE_TMPDIR",
56050
+ "CLAUDE_CODE_USER_DIALOG_TIMEOUT_MS",
55411
56051
  "CLAUDE_CONFIG_DIR",
55412
56052
  "ENABLE_BETA_TRACING_DETAILED",
55413
56053
  "ENABLE_ENHANCED_TELEMETRY_BETA",
@@ -55453,6 +56093,13 @@ const CLAUDECODE_ENV_CONTENT_OFF_VALUES = /* @__PURE__ */ new Set([
55453
56093
  * writing a value Claude Code ignores is the old behavior, while dropping an
55454
56094
  * opt-out it honors would remove a privacy setting. `OTEL_LOG_ASSISTANT_RESPONSES`
55455
56095
  * has no such exception and sits in the plain name table above.
56096
+ *
56097
+ * `CLAUDE_CODE_ENABLE_CFC` follows the same shape for a different reason: a
56098
+ * project cannot turn Claude in Chrome on, since a checked-out repository must
56099
+ * not be able to connect Claude to the user's browser, so only an off value is
56100
+ * kept at project scope.
56101
+ *
56102
+ * @see https://code.claude.com/docs/en/chrome#project-settings-cant-turn-on-chrome
55456
56103
  */
55457
56104
  const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
55458
56105
  OTEL_LOGS_EXPORTER: CLAUDECODE_ENV_EXPORTER_OFF_VALUES,
@@ -55460,7 +56107,8 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
55460
56107
  OTEL_TRACES_EXPORTER: CLAUDECODE_ENV_EXPORTER_OFF_VALUES,
55461
56108
  OTEL_LOG_USER_PROMPTS: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
55462
56109
  OTEL_LOG_TOOL_CONTENT: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
55463
- OTEL_LOG_TOOL_DETAILS: CLAUDECODE_ENV_CONTENT_OFF_VALUES
56110
+ OTEL_LOG_TOOL_DETAILS: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
56111
+ CLAUDE_CODE_ENABLE_CFC: CLAUDECODE_ENV_CONTENT_OFF_VALUES
55464
56112
  };
55465
56113
  function isProjectIgnoredEnvVariable(name, value) {
55466
56114
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_")) return true;
@@ -55497,7 +56145,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
55497
56145
  kept[name] = value;
55498
56146
  }
55499
56147
  if (ignoredEverywhere.length > 0) logger?.warn(`Claude Code permissions: Claude Code ignores ${ignoredEverywhere.map((name) => `'env.${name}'`).join(", ")} in every settings file, so ${ignoredEverywhere.length === 1 ? "it is" : "they are"} not written to ${relativeFilePath}. Set ${ignoredEverywhere.length === 1 ? "it" : "them"} in the environment Claude Code starts from instead, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
55500
- if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, or change how it starts or syncs (a value that turns telemetry off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
56148
+ if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
55501
56149
  return kept;
55502
56150
  }
55503
56151
  /**
@@ -55728,6 +56376,12 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
55728
56376
  relativeFilePath: paths.relativeFilePath,
55729
56377
  logger
55730
56378
  });
56379
+ stripUserScopeOnlyNestedPaths({
56380
+ overrides: overrideTopLevel,
56381
+ global,
56382
+ relativeFilePath: paths.relativeFilePath,
56383
+ logger
56384
+ });
55731
56385
  const { filtered: scopedTopLevel, trustAffecting: trustAffectingTopLevel } = stripUnhonoredTopLevelKeys({
55732
56386
  overrides: overrideTopLevel,
55733
56387
  global,
@@ -56145,7 +56799,7 @@ const CODEBUDDY_RULE_LISTS = [
56145
56799
  "ask",
56146
56800
  "deny"
56147
56801
  ];
56148
- const CATCH_ALL_PATTERN$11 = "*";
56802
+ const CATCH_ALL_PATTERN$12 = "*";
56149
56803
  const MCP_CATEGORY = "mcp";
56150
56804
  const MCP_RULE_PREFIX = "mcp__";
56151
56805
  const ALL_MCP_RULE = "mcp__*";
@@ -56218,7 +56872,7 @@ function categoryOfRule$2(rule) {
56218
56872
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
56219
56873
  */
56220
56874
  function buildCodebuddyRule({ category, pattern, action, logger }) {
56221
- const catchAll = pattern === CATCH_ALL_PATTERN$11;
56875
+ const catchAll = pattern === CATCH_ALL_PATTERN$12;
56222
56876
  if (category === MCP_CATEGORY) {
56223
56877
  if (catchAll && action === "allow") {
56224
56878
  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').`);
@@ -56227,7 +56881,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
56227
56881
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
56228
56882
  }
56229
56883
  if (category === "*") {
56230
- if (catchAll) return CATCH_ALL_PATTERN$11;
56884
+ if (catchAll) return CATCH_ALL_PATTERN$12;
56231
56885
  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').`);
56232
56886
  return null;
56233
56887
  }
@@ -56245,7 +56899,7 @@ function parseCodebuddyRule(rule) {
56245
56899
  const { specifier } = splitCodebuddyRule(rule);
56246
56900
  if (specifier === void 0) return {
56247
56901
  category,
56248
- pattern: CATCH_ALL_PATTERN$11
56902
+ pattern: CATCH_ALL_PATTERN$12
56249
56903
  };
56250
56904
  const trimmed = specifier.trim();
56251
56905
  return trimmed === "" ? null : {
@@ -58496,7 +59150,7 @@ function mapBashActionToDecision(action) {
58496
59150
  //#region src/features/permissions/commandcode-permissions.ts
58497
59151
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
58498
59152
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
58499
- const CATCH_ALL_PATTERN$10 = "*";
59153
+ const CATCH_ALL_PATTERN$11 = "*";
58500
59154
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
58501
59155
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
58502
59156
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -58521,14 +59175,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
58521
59175
  web_fetch: "webfetch",
58522
59176
  web_search: "websearch"
58523
59177
  };
58524
- const ACTION_RANK$4 = {
59178
+ const ACTION_RANK$5 = {
58525
59179
  allow: 0,
58526
59180
  ask: 1,
58527
59181
  deny: 2
58528
59182
  };
58529
59183
  /** Whether `action` outranks `existing` (deny > ask > allow). */
58530
59184
  function isStricterAction({ action, existing }) {
58531
- return existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing];
59185
+ return existing === void 0 || ACTION_RANK$5[action] > ACTION_RANK$5[existing];
58532
59186
  }
58533
59187
  /**
58534
59188
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -58551,10 +59205,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
58551
59205
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
58552
59206
  */
58553
59207
  function buildCommandcodeRule(category, pattern) {
58554
- const catchAll = pattern === CATCH_ALL_PATTERN$10 || pattern === "";
59208
+ const catchAll = pattern === CATCH_ALL_PATTERN$11 || pattern === "";
58555
59209
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
58556
59210
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
58557
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$10 : null;
59211
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$11 : null;
58558
59212
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
58559
59213
  if (tool === void 0) return null;
58560
59214
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -58590,7 +59244,7 @@ function splitCommandcodeRule(rule) {
58590
59244
  }
58591
59245
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
58592
59246
  function isCatchAllSpecifier(inner) {
58593
- return inner === "" || inner === CATCH_ALL_PATTERN$10;
59247
+ return inner === "" || inner === CATCH_ALL_PATTERN$11;
58594
59248
  }
58595
59249
  /**
58596
59250
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -58624,34 +59278,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
58624
59278
  if (server.length === 0) return null;
58625
59279
  const everyServer = {
58626
59280
  category: "mcp",
58627
- pattern: CATCH_ALL_PATTERN$10,
59281
+ pattern: CATCH_ALL_PATTERN$11,
58628
59282
  notInAllow: true
58629
59283
  };
58630
59284
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
58631
- if (server === CATCH_ALL_PATTERN$10) return everyServer;
58632
- if (server.includes(CATCH_ALL_PATTERN$10)) return null;
59285
+ if (server === CATCH_ALL_PATTERN$11) return everyServer;
59286
+ if (server.includes(CATCH_ALL_PATTERN$11)) return null;
58633
59287
  return {
58634
- category: rest === "" || rest === CATCH_ALL_PATTERN$10 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
58635
- pattern: CATCH_ALL_PATTERN$10,
59288
+ category: rest === "" || rest === CATCH_ALL_PATTERN$11 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
59289
+ pattern: CATCH_ALL_PATTERN$11,
58636
59290
  notInAllow: false
58637
59291
  };
58638
59292
  }
58639
59293
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
58640
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$10);
59294
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$11);
58641
59295
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
58642
59296
  category,
58643
59297
  pattern: inner,
58644
59298
  notInAllow: true
58645
59299
  } : null;
58646
- if (remainder === CATCH_ALL_PATTERN$10) return everyServer;
58647
- if (rest === CATCH_ALL_PATTERN$10 && !server.includes(CATCH_ALL_PATTERN$10)) return {
59300
+ if (remainder === CATCH_ALL_PATTERN$11) return everyServer;
59301
+ if (rest === CATCH_ALL_PATTERN$11 && !server.includes(CATCH_ALL_PATTERN$11)) return {
58648
59302
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
58649
- pattern: CATCH_ALL_PATTERN$10,
59303
+ pattern: CATCH_ALL_PATTERN$11,
58650
59304
  notInAllow: true
58651
59305
  };
58652
59306
  return namesTool ? {
58653
59307
  category,
58654
- pattern: CATCH_ALL_PATTERN$10,
59308
+ pattern: CATCH_ALL_PATTERN$11,
58655
59309
  notInAllow: false
58656
59310
  } : null;
58657
59311
  }
@@ -58675,14 +59329,14 @@ function friendlyToolRule({ category, inner }) {
58675
59329
  if (category === "bash") {
58676
59330
  if (isCatchAllSpecifier(inner)) return {
58677
59331
  category,
58678
- pattern: CATCH_ALL_PATTERN$10,
59332
+ pattern: CATCH_ALL_PATTERN$11,
58679
59333
  notInAllow: false
58680
59334
  };
58681
59335
  const normalized = inner.trim().replace(/\s+/g, " ");
58682
59336
  if (normalized === "" || normalized === ":*") return null;
58683
59337
  return isCatchAllSpecifier(normalized) ? {
58684
59338
  category,
58685
- pattern: CATCH_ALL_PATTERN$10,
59339
+ pattern: CATCH_ALL_PATTERN$11,
58686
59340
  notInAllow: true
58687
59341
  } : {
58688
59342
  category,
@@ -58692,7 +59346,7 @@ function friendlyToolRule({ category, inner }) {
58692
59346
  }
58693
59347
  if (isCatchAllSpecifier(inner)) return {
58694
59348
  category,
58695
- pattern: CATCH_ALL_PATTERN$10,
59349
+ pattern: CATCH_ALL_PATTERN$11,
58696
59350
  notInAllow: false
58697
59351
  };
58698
59352
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -58711,9 +59365,9 @@ function friendlyToolRule({ category, inner }) {
58711
59365
  */
58712
59366
  function parseCommandcodeRule(rule) {
58713
59367
  const { tool, inner } = splitCommandcodeRule(rule);
58714
- if (tool === CATCH_ALL_PATTERN$10) return isCatchAllSpecifier(inner) ? {
59368
+ if (tool === CATCH_ALL_PATTERN$11) return isCatchAllSpecifier(inner) ? {
58715
59369
  category: "*",
58716
- pattern: CATCH_ALL_PATTERN$10,
59370
+ pattern: CATCH_ALL_PATTERN$11,
58717
59371
  notInAllow: true
58718
59372
  } : null;
58719
59373
  const lowered = tool.toLowerCase();
@@ -58783,7 +59437,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
58783
59437
  }).filter((rule) => {
58784
59438
  const parsed = parseCommandcodeRule(rule);
58785
59439
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
58786
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$10})`);
59440
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$11})`);
58787
59441
  if (key !== "allow" && isStricterAction({
58788
59442
  action: key,
58789
59443
  existing: replacement
@@ -58840,7 +59494,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
58840
59494
  const ranked = /* @__PURE__ */ new Map();
58841
59495
  const managedCategories = /* @__PURE__ */ new Set();
58842
59496
  for (const [category, rules] of Object.entries(permission)) {
58843
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$10);
59497
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$11);
58844
59498
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
58845
59499
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
58846
59500
  for (const [pattern, action] of Object.entries(rules)) {
@@ -59111,7 +59765,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
59111
59765
  //#endregion
59112
59766
  //#region src/features/permissions/continue-permissions.ts
59113
59767
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
59114
- const CATCH_ALL_PATTERN$9 = "*";
59768
+ const CATCH_ALL_PATTERN$10 = "*";
59115
59769
  /**
59116
59770
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
59117
59771
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -59119,7 +59773,7 @@ const CATCH_ALL_PATTERN$9 = "*";
59119
59773
  * the bare entry instead of being skipped as a pattern-specific rule.
59120
59774
  */
59121
59775
  function normalizeCatchAllPattern$1(pattern) {
59122
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
59776
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
59123
59777
  }
59124
59778
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
59125
59779
  bash: "Bash",
@@ -59156,7 +59810,7 @@ function toCanonicalToolName$6(continueName) {
59156
59810
  * `Tool(pattern)`.
59157
59811
  */
59158
59812
  function buildContinuePermissionEntry(toolName, pattern) {
59159
- return pattern === CATCH_ALL_PATTERN$9 ? toolName : `${toolName}(${pattern})`;
59813
+ return pattern === CATCH_ALL_PATTERN$10 ? toolName : `${toolName}(${pattern})`;
59160
59814
  }
59161
59815
  /**
59162
59816
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -59170,7 +59824,7 @@ function parseContinuePermissionEntry(entry) {
59170
59824
  if (toolName === "") return;
59171
59825
  return {
59172
59826
  toolName,
59173
- pattern: pattern === "" ? CATCH_ALL_PATTERN$9 : pattern
59827
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$10 : pattern
59174
59828
  };
59175
59829
  }
59176
59830
  /**
@@ -59344,7 +59998,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
59344
59998
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
59345
59999
  continue;
59346
60000
  }
59347
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$9) {
60001
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$10) {
59348
60002
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
59349
60003
  continue;
59350
60004
  }
@@ -59712,14 +60366,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
59712
60366
  };
59713
60367
  //#endregion
59714
60368
  //#region src/features/permissions/crush-permissions.ts
59715
- const CATCH_ALL_PATTERN$8 = "*";
60369
+ const CATCH_ALL_PATTERN$9 = "*";
59716
60370
  /**
59717
60371
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
59718
60372
  * argument patterns at all, so it is the only pattern a rule can carry and
59719
60373
  * still be written.
59720
60374
  */
59721
60375
  function normalizeCatchAllPattern(pattern) {
59722
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
60376
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
59723
60377
  }
59724
60378
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
59725
60379
  bash: "bash",
@@ -59918,7 +60572,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
59918
60572
  for (const [rawPattern, action] of Object.entries(rules)) {
59919
60573
  if (isPrototypePollutionKey(rawPattern)) continue;
59920
60574
  const pattern = normalizeCatchAllPattern(rawPattern);
59921
- if (pattern === CATCH_ALL_PATTERN$8) {
60575
+ if (pattern === CATCH_ALL_PATTERN$9) {
59922
60576
  catchAllAction = action;
59923
60577
  continue;
59924
60578
  }
@@ -59991,11 +60645,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
59991
60645
  };
59992
60646
  for (const entry of disabled) {
59993
60647
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
59994
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] = "deny";
60648
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] = "deny";
59995
60649
  }
59996
60650
  for (const entry of allowed) {
59997
60651
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
59998
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] ??= "allow";
60652
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] ??= "allow";
59999
60653
  }
60000
60654
  return { permission };
60001
60655
  }
@@ -61860,7 +62514,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
61860
62514
  //#region src/features/permissions/goose-permissions.ts
61861
62515
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
61862
62516
  const GOOSE_USER_KEY = "user";
61863
- const CATCH_ALL_PATTERN$7 = "*";
62517
+ const CATCH_ALL_PATTERN$8 = "*";
61864
62518
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
61865
62519
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
61866
62520
  shell: ["bash"],
@@ -61872,7 +62526,7 @@ const ACTION_TO_GOOSE_LIST = {
61872
62526
  ask: "ask_before",
61873
62527
  deny: "never_allow"
61874
62528
  };
61875
- const ACTION_RANK$3 = {
62529
+ const ACTION_RANK$4 = {
61876
62530
  allow: 0,
61877
62531
  ask: 1,
61878
62532
  deny: 2
@@ -62024,14 +62678,14 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
62024
62678
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
62025
62679
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
62026
62680
  for (const [pattern, action] of Object.entries(rules)) {
62027
- if (pattern !== CATCH_ALL_PATTERN$7) {
62681
+ if (pattern !== CATCH_ALL_PATTERN$8) {
62028
62682
  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.`);
62029
62683
  continue;
62030
62684
  }
62031
62685
  const previous = assigned.get(toolName);
62032
62686
  if (previous === action) continue;
62033
62687
  if (previous !== void 0) {
62034
- const stricter = ACTION_RANK$3[action] > ACTION_RANK$3[previous] ? action : previous;
62688
+ const stricter = ACTION_RANK$4[action] > ACTION_RANK$4[previous] ? action : previous;
62035
62689
  logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
62036
62690
  if (stricter === previous) continue;
62037
62691
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
@@ -62055,7 +62709,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
62055
62709
  const action = GOOSE_LIST_TO_ACTION[key];
62056
62710
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
62057
62711
  permission[category] ??= {};
62058
- permission[category][CATCH_ALL_PATTERN$7] = action;
62712
+ permission[category][CATCH_ALL_PATTERN$8] = action;
62059
62713
  }
62060
62714
  }
62061
62715
  return { permission };
@@ -62066,7 +62720,7 @@ const GROKCLI_UI_KEY = "ui";
62066
62720
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
62067
62721
  const GROKCLI_PERMISSION_KEY = "permission";
62068
62722
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
62069
- const CATCH_ALL_PATTERN$6 = "*";
62723
+ const CATCH_ALL_PATTERN$7 = "*";
62070
62724
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
62071
62725
  const CATEGORY_TO_GROK_TOOL = {
62072
62726
  bash: "Bash",
@@ -62091,6 +62745,8 @@ const GROK_TOOL_TO_CATEGORY = {
62091
62745
  ...Object.fromEntries(["SendSubagentMessage", "SendAgentMessage"].map((tool) => [tool, "agent"]))
62092
62746
  };
62093
62747
  const GROK_MCP_TOOL = "MCPTool";
62748
+ const GROK_ALL_TOOLS_ENTRY = "*";
62749
+ const GROK_VERBOSE_ANY_TOOL = "any";
62094
62750
  const GROK_TOOL_TO_CATEGORY_LOWER = {
62095
62751
  ...Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category])),
62096
62752
  agent_message: "agent"
@@ -62105,22 +62761,26 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
62105
62761
  * Scoped MCP categories (`mcp__<remainder>`) fold their address into the
62106
62762
  * parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
62107
62763
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
62108
- * concrete pattern emits `Tool(pattern)`.
62764
+ * concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
62765
+ * Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
62766
+ * under it has no Grok spelling and returns `null`.
62109
62767
  */
62110
62768
  function buildGrokEntry(category, pattern) {
62769
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$7 ? GROK_ALL_TOOLS_ENTRY : null;
62111
62770
  if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
62112
62771
  const remainder = category.slice(5);
62113
62772
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
62114
62773
  }
62115
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
62774
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
62116
62775
  const tool = CATEGORY_TO_GROK_TOOL[category];
62117
62776
  if (tool === void 0) return null;
62118
- return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? tool : `${tool}(${pattern})`;
62777
+ return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? tool : `${tool}(${pattern})`;
62119
62778
  }
62120
62779
  /**
62121
62780
  * Parse a Grok Claude-style permission entry back into a canonical category +
62122
- * pattern. Entries without parentheses are treated as catch-all (`*`). Returns
62123
- * `null` for tool prefixes with no canonical equivalent (e.g. `any`).
62781
+ * pattern. Entries without parentheses are treated as catch-all (`*`), and the
62782
+ * bare `*` rule becomes the all-tools `*` category. Returns `null` for tool
62783
+ * prefixes with no canonical equivalent.
62124
62784
  */
62125
62785
  function parseGrokEntry(entry) {
62126
62786
  const trimmed = entry.trim();
@@ -62134,19 +62794,23 @@ function parseGrokEntry(entry) {
62134
62794
  tool = trimmed.slice(0, parenIndex);
62135
62795
  inner = trimmed.slice(parenIndex + 1, -1).trim();
62136
62796
  }
62797
+ if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
62798
+ category: "*",
62799
+ pattern: CATCH_ALL_PATTERN$7
62800
+ };
62137
62801
  if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$3)) return parseClaudeStyleMcpEntry(tool);
62138
62802
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
62139
62803
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
62140
- pattern: CATCH_ALL_PATTERN$6
62804
+ pattern: CATCH_ALL_PATTERN$7
62141
62805
  } : {
62142
62806
  category: "mcp",
62143
- pattern: CATCH_ALL_PATTERN$6
62807
+ pattern: CATCH_ALL_PATTERN$7
62144
62808
  };
62145
62809
  const category = GROK_TOOL_TO_CATEGORY[tool];
62146
62810
  if (category === void 0) return null;
62147
62811
  return {
62148
62812
  category,
62149
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$6
62813
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$7
62150
62814
  };
62151
62815
  }
62152
62816
  /**
@@ -62163,14 +62827,14 @@ function parseGrokEntry(entry) {
62163
62827
  function parseClaudeStyleMcpEntry(entry) {
62164
62828
  const rest = entry.slice(5);
62165
62829
  if (rest.length === 0) return null;
62166
- if (rest === CATCH_ALL_PATTERN$6) return {
62830
+ if (rest === CATCH_ALL_PATTERN$7) return {
62167
62831
  category: "mcp",
62168
- pattern: CATCH_ALL_PATTERN$6
62832
+ pattern: CATCH_ALL_PATTERN$7
62169
62833
  };
62170
62834
  const address = rest.includes("__") ? rest : `${rest}__*`;
62171
62835
  return {
62172
62836
  category: `${MCP_CANONICAL_PREFIX$3}${address}`,
62173
- pattern: CATCH_ALL_PATTERN$6
62837
+ pattern: CATCH_ALL_PATTERN$7
62174
62838
  };
62175
62839
  }
62176
62840
  /**
@@ -62191,15 +62855,20 @@ function parseGrokRule(rule) {
62191
62855
  if (typeof tool !== "string") return null;
62192
62856
  if (pattern !== void 0 && typeof pattern !== "string") return null;
62193
62857
  const trimmedPattern = pattern?.trim() ?? "";
62194
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$6;
62858
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$7;
62195
62859
  const lowerTool = tool.trim().toLowerCase();
62196
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$6 ? {
62860
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
62861
+ category: "*",
62862
+ pattern: CATCH_ALL_PATTERN$7,
62863
+ action
62864
+ } : null;
62865
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
62197
62866
  category: "mcp",
62198
- pattern: CATCH_ALL_PATTERN$6,
62867
+ pattern: CATCH_ALL_PATTERN$7,
62199
62868
  action
62200
62869
  } : {
62201
62870
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
62202
- pattern: CATCH_ALL_PATTERN$6,
62871
+ pattern: CATCH_ALL_PATTERN$7,
62203
62872
  action
62204
62873
  };
62205
62874
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -62229,7 +62898,10 @@ function parseGrokRule(rule) {
62229
62898
  * subagent rather than its launch, and whose pattern is a subagent id
62230
62899
  * rather than a subagent type); `mcp__*` maps to `MCPTool(...)` (a scoped
62231
62900
  * MCP category folds its address into the parentheses, so a non-`*`
62232
- * argument pattern on it is not represented). The one category with no Grok tool (`notebookedit`) is
62901
+ * argument pattern on it is not represented); the all-tools `*` category
62902
+ * maps to Grok's bare `*` rule (which matches every tool) for its
62903
+ * catch-all pattern, while a narrower `*` pattern is skipped with a
62904
+ * warning. The one category with no Grok tool (`notebookedit`) is
62233
62905
  * skipped (with a warning when it carries a `deny` rule, to surface the
62234
62906
  * gap). When two canonical rules collapse onto the same Grok
62235
62907
  * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
@@ -62240,7 +62912,9 @@ function parseGrokRule(rule) {
62240
62912
  * verbose `[[permission.rules]]` tables
62241
62913
  * (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
62242
62914
  * field is matched case-insensitively against the same tool table, plus
62243
- * the documented `mcp` alias for the compact form's `MCPTool`. Rules
62915
+ * the documented `mcp` alias for the compact form's `MCPTool`. A bare `*`
62916
+ * entry and a tool-wide verbose `any` rule both import as the all-tools
62917
+ * `*` category. Rules
62244
62918
  * from the two forms are merged with the same `deny > ask > allow`
62245
62919
  * precedence. When neither form carries a rule (older configs), the coarse
62246
62920
  * `[ui] permission_mode` is used as a fallback. Generate always emits the
@@ -62265,7 +62939,7 @@ function parseGrokRule(rule) {
62265
62939
  * tools it models and (in global scope) the `[ui] permission_mode` value, while
62266
62940
  * every other key
62267
62941
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
62268
- * entries for tool prefixes rulesync cannot model (e.g. `any`) are preserved.
62942
+ * entries for tool prefixes rulesync cannot model (e.g. a pattern-only entry such as `src/**`) are preserved.
62269
62943
  * The file is never deleted.
62270
62944
  */
62271
62945
  var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
@@ -62347,7 +63021,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
62347
63021
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
62348
63022
  }
62349
63023
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
62350
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$6]: legacyModeAction(parsed) } } };
63024
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$7]: legacyModeAction(parsed) } } };
62351
63025
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
62352
63026
  }
62353
63027
  validate() {
@@ -62367,14 +63041,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
62367
63041
  });
62368
63042
  }
62369
63043
  };
62370
- const ACTION_RANK$2 = {
63044
+ const ACTION_RANK$3 = {
62371
63045
  allow: 0,
62372
63046
  ask: 1,
62373
63047
  deny: 2
62374
63048
  };
62375
63049
  /**
62376
63050
  * Collect user-authored entries from an existing `[permission]` array whose
62377
- * tool prefix rulesync cannot model (e.g. `any`). Such entries are
63051
+ * tool prefix rulesync cannot model (e.g. a pattern-only entry such as `src/**`). Such entries are
62378
63052
  * preserved verbatim so replacing the arrays does not silently drop them
62379
63053
  * (mirrors the Cursor adapter's preservation of unmanaged types).
62380
63054
  */
@@ -62393,13 +63067,26 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
62393
63067
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
62394
63068
  const entry = buildGrokEntry(category, pattern);
62395
63069
  if (entry === null) {
63070
+ if (category === "*") {
63071
+ logger?.warn(`Grok CLI permissions: skipping the all-tools '*' rule '${pattern}' — Grok's bare '*' rule takes no pattern. Write it under the categories it is meant for (for example 'bash' or 'read').`);
63072
+ continue;
63073
+ }
62396
63074
  if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
62397
63075
  continue;
62398
63076
  }
62399
63077
  const existing = ranked.get(entry);
62400
63078
  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).`);
62401
- if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) ranked.set(entry, action);
63079
+ if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) ranked.set(entry, action);
62402
63080
  }
63081
+ warnRemovedAllToolsRestrictions({
63082
+ existingPermission,
63083
+ ranked,
63084
+ logger
63085
+ });
63086
+ warnEntriesShadowedByAllTools({
63087
+ ranked,
63088
+ logger
63089
+ });
62403
63090
  const allow = unmanagedEntries(existingPermission, "allow");
62404
63091
  const deny = unmanagedEntries(existingPermission, "deny");
62405
63092
  const ask = unmanagedEntries(existingPermission, "ask");
@@ -62413,6 +63100,38 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
62413
63100
  };
62414
63101
  }
62415
63102
  /**
63103
+ * Warn when generate drops an existing bare `*` `deny`/`ask` entry without
63104
+ * writing an equally strict one back. Such an entry is Rulesync-owned (it maps
63105
+ * to the all-tools `*` category), so a config without a matching canonical
63106
+ * rule removes it — which silently lifts a restriction on every tool.
63107
+ */
63108
+ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger }) {
63109
+ if (!logger) return;
63110
+ const generated = ranked.get(GROK_ALL_TOOLS_ENTRY);
63111
+ for (const action of ["deny", "ask"]) {
63112
+ const entries = existingPermission[action];
63113
+ if (!isStringArray$2(entries)) continue;
63114
+ if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63115
+ if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
63116
+ 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.`);
63117
+ }
63118
+ }
63119
+ /**
63120
+ * Warn about entries that a bare `*` `deny`/`ask` makes ineffective: Grok
63121
+ * evaluates by severity (`deny > ask > allow`), so a specific `allow` (and,
63122
+ * under a `*` deny, a specific `ask`) never takes effect next to it. Shell
63123
+ * rules are already folded by `honorAllToolsOnBash`, so this covers the other
63124
+ * tools.
63125
+ */
63126
+ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63127
+ if (!logger) return;
63128
+ const allTools = ranked.get(GROK_ALL_TOOLS_ENTRY);
63129
+ if (allTools === void 0 || allTools === "allow") return;
63130
+ const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$3[action] < ACTION_RANK$3[allTools]).map(([entry]) => entry).toSorted();
63131
+ if (shadowed.length === 0) return;
63132
+ 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(", ")}.`);
63133
+ }
63134
+ /**
62416
63135
  * Parse Grok's `[permission]` section back into a canonical permission map,
62417
63136
  * reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
62418
63137
  * Claude-style entries and the verbose `[[permission.rules]]` tables
@@ -62431,7 +63150,7 @@ function parseGrokPermissionArrays(permission) {
62431
63150
  const record = ({ category, pattern, action }) => {
62432
63151
  const bucket = result[category] ??= {};
62433
63152
  const existing = bucket[pattern];
62434
- if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) bucket[pattern] = action;
63153
+ if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) bucket[pattern] = action;
62435
63154
  };
62436
63155
  const applyEntries = (entries, action) => {
62437
63156
  for (const entry of entries ?? []) {
@@ -62835,7 +63554,7 @@ const JUNIE_GROUP_TO_CANONICAL = {
62835
63554
  mcpTools: "mcp",
62836
63555
  readOutsideProject: "read"
62837
63556
  };
62838
- function isPermissionAction$1(value) {
63557
+ function isPermissionAction$2(value) {
62839
63558
  return PermissionActionSchema.safeParse(value).success;
62840
63559
  }
62841
63560
  /**
@@ -63021,7 +63740,7 @@ function convertJunieToRulesyncPermissions({ allowlist }) {
63021
63740
  for (const rule of Array.isArray(ruleSet.rules) ? ruleSet.rules : []) {
63022
63741
  if (!rule || typeof rule !== "object") continue;
63023
63742
  const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
63024
- if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
63743
+ if (pattern === void 0 || !isPermissionAction$2(rule.action)) continue;
63025
63744
  (permission[category] ??= {})[pattern] = rule.action;
63026
63745
  }
63027
63746
  }
@@ -63980,6 +64699,367 @@ function rulesFromArrays(settings, allowKey, denyKey) {
63980
64699
  return rules;
63981
64700
  }
63982
64701
  //#endregion
64702
+ //#region src/features/permissions/kiro-capability-permissions.ts
64703
+ const CATCH_ALL_PATTERN$6 = "*";
64704
+ const CANONICAL_TO_KIRO_CAPABILITY = {
64705
+ bash: "shell",
64706
+ read: "fs_read",
64707
+ edit: "fs_write",
64708
+ write: "fs_write",
64709
+ webfetch: "web_fetch",
64710
+ websearch: "web_search"
64711
+ };
64712
+ const KIRO_CAPABILITY_TO_CANONICAL = {
64713
+ shell: "bash",
64714
+ fs_read: "read",
64715
+ fs_write: "write",
64716
+ web_fetch: "webfetch",
64717
+ web_search: "websearch"
64718
+ };
64719
+ const MANAGED_CAPABILITIES = new Set(Object.keys(KIRO_CAPABILITY_TO_CANONICAL));
64720
+ const ACTION_RANK$2 = {
64721
+ allow: 0,
64722
+ ask: 1,
64723
+ deny: 2
64724
+ };
64725
+ function isPermissionAction$1(value) {
64726
+ return value === "allow" || value === "ask" || value === "deny";
64727
+ }
64728
+ /**
64729
+ * Whether an existing `permissions.yaml` rule can be read back into the
64730
+ * canonical model: a rule for a capability rulesync maps, with no `exclude`
64731
+ * list (the canonical model has no exclusion). Used on import only.
64732
+ */
64733
+ function isImportableRule(rule) {
64734
+ return isRecord$1(rule) && typeof rule.capability === "string" && MANAGED_CAPABILITIES.has(rule.capability) && rule.exclude === void 0;
64735
+ }
64736
+ /**
64737
+ * Key identifying one (capability, pattern) pair. The catch-all `*` stands for
64738
+ * a match-less rule.
64739
+ */
64740
+ function rulePatternKey(capability, pattern) {
64741
+ return `${capability}\u0000${pattern}`;
64742
+ }
64743
+ /**
64744
+ * Classify one existing (capability, pattern, effect) entry against the
64745
+ * generated rules: an exact duplicate (same effect) is replaced by the
64746
+ * generated rule; a different effect is a conflict and is kept; anything
64747
+ * rulesync does not emit is unrelated and kept.
64748
+ */
64749
+ function classifyExistingEntry({ emitted, capability, pattern, effect }) {
64750
+ const effects = emitted.get(rulePatternKey(capability, pattern));
64751
+ if (effects === void 0) return "unrelated";
64752
+ return isPermissionAction$1(effect) && effects.has(effect) ? "duplicate" : "conflict";
64753
+ }
64754
+ /**
64755
+ * Merge one existing rule against the generated entries (see
64756
+ * {@link mergeKiroCapabilityRules}). Returns the rule to keep (possibly with a
64757
+ * narrowed `match`), or `undefined` when every entry is an exact duplicate, and
64758
+ * how many entries rulesync does not generate.
64759
+ */
64760
+ function mergeExistingRule({ rule, emitted, logger }) {
64761
+ if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$2(rule.match)) return {
64762
+ rule,
64763
+ unrelatedEntries: 0
64764
+ };
64765
+ const capability = rule.capability;
64766
+ const isCatchAll = rule.match === void 0;
64767
+ const patterns = isStringArray$2(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
64768
+ if (patterns.length === 0) return {
64769
+ rule,
64770
+ unrelatedEntries: 0
64771
+ };
64772
+ const remaining = [];
64773
+ let unrelatedEntries = 0;
64774
+ for (const pattern of patterns) {
64775
+ const outcome = classifyExistingEntry({
64776
+ emitted,
64777
+ capability,
64778
+ pattern,
64779
+ effect: rule.effect
64780
+ });
64781
+ if (outcome === "duplicate") continue;
64782
+ remaining.push(pattern);
64783
+ if (outcome === "unrelated") {
64784
+ unrelatedEntries++;
64785
+ continue;
64786
+ }
64787
+ logger?.warn(`Kiro permissions.yaml already has a ${capability} rule for "${pattern}" with effect "${String(rule.effect)}"; keeping it alongside the generated rule. Kiro applies the stricter effect (deny > ask > allow).`);
64788
+ }
64789
+ if (remaining.length === 0) return {
64790
+ rule: void 0,
64791
+ unrelatedEntries
64792
+ };
64793
+ if (remaining.length === patterns.length || isCatchAll) return {
64794
+ rule,
64795
+ unrelatedEntries
64796
+ };
64797
+ return {
64798
+ rule: {
64799
+ ...rule,
64800
+ match: remaining
64801
+ },
64802
+ unrelatedEntries
64803
+ };
64804
+ }
64805
+ /**
64806
+ * Merge the existing rules with the generated ones. Rulesync keeps no
64807
+ * ownership record in the file, so it only removes what it is about to write
64808
+ * again: an existing rule loses the `match` entries that exactly duplicate a
64809
+ * generated (capability, pattern, effect) — a match-less rule counts as the `*`
64810
+ * pattern — and is dropped only when no entry is left.
64811
+ *
64812
+ * Everything else is kept verbatim: rules Kiro saved ("Always allow/deny"),
64813
+ * hand-written rules, rules with `exclude`, rules for capabilities rulesync
64814
+ * does not map, malformed entries. An existing rule with the same pattern but a
64815
+ * different effect is kept too, with a warning: Kiro resolves deny > ask >
64816
+ * allow, so the stricter rule still wins and a user's `deny` is never loosened.
64817
+ * Rerunning generate is therefore idempotent.
64818
+ */
64819
+ function mergeKiroCapabilityRules({ existingRules, generatedRules, logger }) {
64820
+ const emitted = /* @__PURE__ */ new Map();
64821
+ for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$6]) {
64822
+ const key = rulePatternKey(rule.capability, pattern);
64823
+ const effects = emitted.get(key) ?? /* @__PURE__ */ new Set();
64824
+ effects.add(rule.effect);
64825
+ emitted.set(key, effects);
64826
+ }
64827
+ const kept = [];
64828
+ let unrelatedManagedEntries = 0;
64829
+ for (const rule of existingRules) {
64830
+ const result = mergeExistingRule({
64831
+ rule,
64832
+ emitted,
64833
+ logger
64834
+ });
64835
+ if (result.rule !== void 0) kept.push(result.rule);
64836
+ unrelatedManagedEntries += result.unrelatedEntries;
64837
+ }
64838
+ if (unrelatedManagedEntries > 0) logger?.warn(`Kiro permissions.yaml keeps ${unrelatedManagedEntries} existing shell/fs_read/fs_write/web_fetch/web_search rule ${unrelatedManagedEntries === 1 ? "entry" : "entries"} that .rulesync/permissions.json does not generate. Rulesync never removes rules from this file and cannot tell which of these it wrote earlier; delete any stale ones by hand.`);
64839
+ return [...kept, ...generatedRules];
64840
+ }
64841
+ const LOCATION_INDEPENDENT_FS_PATTERN = /^(?:\/|~|\*\*|[A-Za-z]:[\\/])/;
64842
+ /**
64843
+ * Permissions adapter for Kiro's capability-based `permissions.yaml` (Kiro IDE
64844
+ * 1.0, and the same engine in Kiro CLI V3). Used for the `kiro-cli` and
64845
+ * `kiro-ide` targets in **global** scope, where it writes the user-level
64846
+ * `~/.kiro/settings/permissions.yaml`. Project scope keeps writing the agent
64847
+ * config `.kiro/agents/default.json` through {@link KiroPermissions}: the
64848
+ * workspace-level `permissions.yaml` lives under a hash-derived directory in the
64849
+ * home directory, not in the repository, so it cannot be generated.
64850
+ *
64851
+ * Mapping (rulesync canonical -> Kiro rule):
64852
+ * - Category -> `capability`: `bash` -> `shell`, `read` -> `fs_read`,
64853
+ * `edit`/`write` -> `fs_write`, `webfetch` -> `web_fetch`, `websearch` ->
64854
+ * `web_search`. Other categories are skipped with a warning.
64855
+ * - Action -> `effect` 1:1, including `ask`.
64856
+ * - Pattern -> `match` entry. The catch-all `*` becomes a rule with no `match`
64857
+ * (the whole capability), because a filesystem `*` only matches one path
64858
+ * component in Kiro.
64859
+ *
64860
+ * Kiro also writes into this file (consent decisions persisted globally), so it
64861
+ * is merged, never replaced or deleted: every top-level key other than `rules`
64862
+ * is preserved, and existing rules are kept except for entries that exactly
64863
+ * repeat a generated rule (see {@link mergeKiroCapabilityRules}). Rulesync
64864
+ * never retracts a rule an earlier run wrote; it warns instead. A run that
64865
+ * changes nothing returns the existing content verbatim; otherwise the file is
64866
+ * re-serialized and its YAML comments are lost.
64867
+ *
64868
+ * @see https://kiro.dev/docs/permissions/
64869
+ */
64870
+ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPermissions {
64871
+ constructor(params) {
64872
+ super({
64873
+ ...params,
64874
+ fileContent: params.fileContent ?? ""
64875
+ });
64876
+ }
64877
+ isDeletable() {
64878
+ return false;
64879
+ }
64880
+ shouldSkipCreationWhenPayloadEmpty() {
64881
+ return true;
64882
+ }
64883
+ static getSettablePaths(_options) {
64884
+ return {
64885
+ relativeDirPath: KIRO_SETTINGS_DIR_PATH,
64886
+ relativeFilePath: KIRO_PERMISSIONS_FILE_NAME
64887
+ };
64888
+ }
64889
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
64890
+ const paths = this.getSettablePaths({ global: true });
64891
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
64892
+ return new KiroCapabilityPermissions({
64893
+ outputRoot,
64894
+ relativeDirPath: paths.relativeDirPath,
64895
+ relativeFilePath: paths.relativeFilePath,
64896
+ fileContent,
64897
+ validate,
64898
+ global: true
64899
+ });
64900
+ }
64901
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
64902
+ const paths = this.getSettablePaths({ global: true });
64903
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
64904
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
64905
+ let parsed;
64906
+ try {
64907
+ parsed = loadYaml(existingContent) ?? {};
64908
+ } catch (error) {
64909
+ throw new Error(`Failed to parse existing Kiro permissions.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
64910
+ }
64911
+ if (!isRecord$1(parsed)) throw new Error(`Failed to parse existing Kiro permissions.yaml at ${filePath}: not a map`);
64912
+ if (parsed.rules !== void 0 && !Array.isArray(parsed.rules)) throw new Error(`Failed to parse existing Kiro permissions.yaml at ${filePath}: 'rules' is not a list`);
64913
+ const config = rulesyncPermissions.getJson();
64914
+ if (config.kiro?.toolsSettings !== void 0) logger?.warn("Kiro permissions: 'kiro.toolsSettings' applies only to the project-scope agent config (.kiro/agents/default.json); ignoring it for ~/.kiro/settings/permissions.yaml.");
64915
+ const generatedRules = buildKiroCapabilityRules({
64916
+ config,
64917
+ logger
64918
+ });
64919
+ const rules = mergeKiroCapabilityRules({
64920
+ existingRules: parsed.rules ?? [],
64921
+ generatedRules,
64922
+ logger
64923
+ });
64924
+ const next = { ...parsed };
64925
+ if (rules.length > 0 || parsed.rules !== void 0) next.rules = rules;
64926
+ return new KiroCapabilityPermissions({
64927
+ outputRoot,
64928
+ relativeDirPath: paths.relativeDirPath,
64929
+ relativeFilePath: paths.relativeFilePath,
64930
+ fileContent: serializeKiroPermissions({
64931
+ next,
64932
+ parsed,
64933
+ existingContent
64934
+ }),
64935
+ validate: true,
64936
+ global: true
64937
+ });
64938
+ }
64939
+ toRulesyncPermissions() {
64940
+ let parsed;
64941
+ try {
64942
+ parsed = loadYaml(this.getFileContent()) ?? {};
64943
+ } catch (error) {
64944
+ throw new Error(`Failed to parse Kiro permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
64945
+ }
64946
+ const rules = isRecord$1(parsed) && Array.isArray(parsed.rules) ? parsed.rules : [];
64947
+ const permission = {};
64948
+ for (const rule of rules) {
64949
+ if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
64950
+ const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
64951
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$2(rule.match) ? rule.match : [];
64952
+ const categoryRules = permission[category] ??= {};
64953
+ for (const pattern of patterns) {
64954
+ const previous = categoryRules[pattern];
64955
+ if (previous === void 0 || ACTION_RANK$2[rule.effect] > ACTION_RANK$2[previous]) categoryRules[pattern] = rule.effect;
64956
+ }
64957
+ }
64958
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
64959
+ }
64960
+ validate() {
64961
+ return {
64962
+ success: true,
64963
+ error: null
64964
+ };
64965
+ }
64966
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
64967
+ return new KiroCapabilityPermissions({
64968
+ outputRoot,
64969
+ relativeDirPath,
64970
+ relativeFilePath,
64971
+ fileContent: "",
64972
+ validate: false,
64973
+ global: true
64974
+ });
64975
+ }
64976
+ };
64977
+ /**
64978
+ * Convert the canonical `permission` block into Kiro capability rules: one rule
64979
+ * per (capability, effect) carrying every non-catch-all pattern as `match`, plus
64980
+ * a match-less rule when the catch-all `*` is set.
64981
+ */
64982
+ function buildKiroCapabilityRules({ config, logger }) {
64983
+ const buckets = /* @__PURE__ */ new Map();
64984
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
64985
+ const capability = CANONICAL_TO_KIRO_CAPABILITY[category];
64986
+ if (capability === void 0) {
64987
+ logger?.warn(`Kiro permissions.yaml has no capability for category: ${category}. Skipping.`);
64988
+ continue;
64989
+ }
64990
+ for (const [pattern, effect] of Object.entries(rules)) {
64991
+ warnOnWorkspaceRelativeFsPattern({
64992
+ capability,
64993
+ category,
64994
+ pattern,
64995
+ logger
64996
+ });
64997
+ const key = `${capability}\u0000${effect}`;
64998
+ const bucket = buckets.get(key) ?? {
64999
+ capability,
65000
+ effect,
65001
+ patterns: /* @__PURE__ */ new Set()
65002
+ };
65003
+ bucket.patterns.add(pattern);
65004
+ buckets.set(key, bucket);
65005
+ }
65006
+ }
65007
+ const result = [];
65008
+ for (const { capability, effect, patterns } of buckets.values()) {
65009
+ if (patterns.has(CATCH_ALL_PATTERN$6)) result.push({
65010
+ capability,
65011
+ effect
65012
+ });
65013
+ const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$6);
65014
+ if (match.length > 0) result.push({
65015
+ capability,
65016
+ match,
65017
+ effect
65018
+ });
65019
+ }
65020
+ return result;
65021
+ }
65022
+ /**
65023
+ * Serialize the merged config. When the merge changes nothing, the existing
65024
+ * content is returned verbatim so a run with nothing to change never rewrites
65025
+ * the file (and never drops its YAML comments).
65026
+ */
65027
+ function serializeKiroPermissions({ next, parsed, existingContent }) {
65028
+ if ((0, node_util.isDeepStrictEqual)(next, parsed)) return existingContent;
65029
+ return Object.keys(next).length === 0 ? "" : (0, js_yaml.dump)(next);
65030
+ }
65031
+ /**
65032
+ * `~/.kiro/settings/permissions.yaml` applies to every workspace, so a relative
65033
+ * filesystem pattern such as `src/**` is not tied to the project it was written
65034
+ * for. Warn so the author can make it absolute, `~`-relative or `**`-prefixed.
65035
+ */
65036
+ function warnOnWorkspaceRelativeFsPattern({ capability, category, pattern, logger }) {
65037
+ if (capability !== "fs_read" && capability !== "fs_write") return;
65038
+ if (pattern === CATCH_ALL_PATTERN$6 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
65039
+ 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.`);
65040
+ }
65041
+ /**
65042
+ * Permissions factory for the `kiro-cli` / `kiro-ide` targets: project scope
65043
+ * writes the agent config `.kiro/agents/default.json` ({@link KiroPermissions}),
65044
+ * global scope writes `~/.kiro/settings/permissions.yaml`
65045
+ * ({@link KiroCapabilityPermissions}). The deprecated `kiro` alias stays on
65046
+ * {@link KiroPermissions} only.
65047
+ */
65048
+ const KiroScopedPermissions = {
65049
+ getSettablePaths(options = {}) {
65050
+ return options.global ? KiroCapabilityPermissions.getSettablePaths(options) : KiroPermissions.getSettablePaths(options);
65051
+ },
65052
+ fromFile(params) {
65053
+ return params.global ? KiroCapabilityPermissions.fromFile(params) : KiroPermissions.fromFile(params);
65054
+ },
65055
+ fromRulesyncPermissions(params) {
65056
+ return params.global ? KiroCapabilityPermissions.fromRulesyncPermissions(params) : KiroPermissions.fromRulesyncPermissions(params);
65057
+ },
65058
+ forDeletion(params) {
65059
+ return params.global ? KiroCapabilityPermissions.forDeletion(params) : KiroPermissions.forDeletion(params);
65060
+ }
65061
+ };
65062
+ //#endregion
63983
65063
  //#region src/features/permissions/lettacode-permissions.ts
63984
65064
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
63985
65065
  const LETTACODE_PERMISSIONS_KEY = "permissions";
@@ -66007,6 +67087,8 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
66007
67087
  "eager",
66008
67088
  "listDirectory",
66009
67089
  "todoWrite",
67090
+ "codeModeOnly",
67091
+ "freeform",
66010
67092
  "workflowsEnabled",
66011
67093
  "workflowNameOnly",
66012
67094
  "executionSandbox"
@@ -66133,6 +67215,16 @@ const QWEN_SCOPED_TOOLS_KEYS = {
66133
67215
  rule: "global-machine-wide",
66134
67216
  projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`todo_write\` tool is registered in this repository.`,
66135
67217
  globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
67218
+ },
67219
+ codeModeOnly: {
67220
+ rule: "global-machine-wide",
67221
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether ordinary tools are reached only through Code Mode's \`exec\` tool in this repository.`,
67222
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether ordinary tools are reached only through Code Mode's `exec` tool for every project on this machine."
67223
+ },
67224
+ freeform: {
67225
+ rule: "global-machine-wide",
67226
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether Code Mode's \`exec\` tool takes raw text input on OpenAI Responses models in this repository.`,
67227
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether Code Mode's `exec` tool takes raw text input on OpenAI Responses models for every project on this machine."
66136
67228
  }
66137
67229
  };
66138
67230
  /**
@@ -66596,22 +67688,42 @@ function convertQwenToRulesyncPermissions(params) {
66596
67688
  //#endregion
66597
67689
  //#region src/features/permissions/reasonix-permissions.ts
66598
67690
  /**
66599
- * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
66600
- * permission-rule tool families (PascalCase).
66601
- *
66602
- * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
66603
- * documented as "Claude Code-style": "Bash and file mutation approvals use
66604
- * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
66605
- * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
66606
- * IDs for compatibility, but new rules are saved using these PascalCase
66607
- * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
66608
- * uses (the closest documented precedent for this syntax).
66609
- * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
67691
+ * Mapping from rulesync canonical tool category names (lowercase) to the tool
67692
+ * names a Reasonix `[permissions]` rule must use.
67693
+ *
67694
+ * Reasonix matches a rule's tool exactly against its registered tools, which
67695
+ * are snake_case (`read_file`, `write_file`, `grep`, `glob`, `web_fetch`,
67696
+ * `web_search`, `notebook_edit`, `task`). Only `Bash`, `Edit` (every file
67697
+ * writer: `write_file`, `edit_file`, `multi_edit`, `notebook_edit`, ...),
67698
+ * `Browser` and `Computer` are Claude-Code-style family aliases
67699
+ * (`canonicalRuleTool` / `groupOf`), so a `Read(...)` or `Write(...)` rule
67700
+ * matches nothing and Reasonix v2.34.0+ reports it as dormant. Checked on both
67701
+ * release lines: v1.39.8 (`internal/permission/permission.go`) and v2.41.0
67702
+ * (`internal/safety/permission/permission.go`, `rule_group.go`).
67703
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.41.0/internal/safety/permission/permission.go
67704
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.41.0/internal/safety/permission/rule_group.go
66610
67705
  */
66611
67706
  const CANONICAL_TO_REASONIX_TOOL_NAMES = {
66612
67707
  bash: "Bash",
66613
- read: "Read",
67708
+ read: REASONIX_READ_TOOL_NAME,
66614
67709
  edit: "Edit",
67710
+ write: "write_file",
67711
+ webfetch: "web_fetch",
67712
+ websearch: "web_search",
67713
+ grep: "grep",
67714
+ glob: "glob",
67715
+ notebookedit: "notebook_edit",
67716
+ agent: "task"
67717
+ };
67718
+ /**
67719
+ * The Claude-Code-style spellings rulesync emitted before it followed the
67720
+ * Reasonix tool vocabulary. None of them reaches a Reasonix tool, so they are
67721
+ * read back on import (a file generated by an older rulesync still imports)
67722
+ * and counted as managed on generate (the dormant copy is replaced rather than
67723
+ * kept beside the working rule).
67724
+ */
67725
+ const CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES = {
67726
+ read: REASONIX_LEGACY_READ_TOOL_NAME,
66615
67727
  write: "Write",
66616
67728
  webfetch: "WebFetch",
66617
67729
  websearch: "WebSearch",
@@ -66621,9 +67733,16 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
66621
67733
  agent: "Agent"
66622
67734
  };
66623
67735
  /**
66624
- * Reverse mapping from Reasonix tool names to rulesync canonical names.
67736
+ * Reverse mapping from Reasonix tool names (current and legacy spellings) to
67737
+ * rulesync canonical names.
66625
67738
  */
66626
- const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
67739
+ const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries([...Object.entries(CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]), ...Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k])]);
67740
+ /** Every Reasonix tool name a canonical category owns on generate. */
67741
+ function reasonixToolNamesOf(canonical) {
67742
+ const legacy = CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES[canonical];
67743
+ const current = toReasonixToolName(canonical);
67744
+ return legacy === void 0 ? [current] : [current, legacy];
67745
+ }
66627
67746
  function toReasonixToolName(canonical) {
66628
67747
  return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
66629
67748
  }
@@ -66896,7 +68015,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
66896
68015
  const parsed = parseReasonixConfig(existingContent);
66897
68016
  const config = rulesyncPermissions.getJson();
66898
68017
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
66899
- const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
68018
+ const managedToolNames = new Set(Object.keys(config.permission).flatMap((category) => reasonixToolNamesOf(category)));
66900
68019
  const override = config.reasonix;
66901
68020
  const rawAllow = toReasonixStringArray(override?.rawAllow);
66902
68021
  const rawAsk = toReasonixStringArray(override?.rawAsk);
@@ -66913,12 +68032,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
66913
68032
  rawOwnedEntries,
66914
68033
  logger
66915
68034
  });
66916
- if (logger && managedToolNames.has("Read")) {
68035
+ if (logger && managedToolNames.has("read_file")) {
68036
+ const readToolNames = new Set(reasonixToolNamesOf("read"));
66917
68037
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
66918
68038
  const { toolName } = parseReasonixPermissionEntry(entry);
66919
- return toolName === "Read";
68039
+ return readToolNames.has(toolName);
66920
68040
  });
66921
- if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages 'Read' tool and will overwrite ${droppedReadDenyEntries.length} existing Read deny entries (possibly from ignore feature). Permissions take precedence.`);
68041
+ if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages '${REASONIX_READ_TOOL_NAME}' tool and will overwrite ${droppedReadDenyEntries.length} existing read deny entries (possibly from ignore feature). Permissions take precedence.`);
66922
68042
  }
66923
68043
  const mergedPermissions = { ...existingPermissions };
66924
68044
  setOrDeleteEntries(mergedPermissions, "allow", [
@@ -72264,18 +73384,18 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
72264
73384
  }
72265
73385
  }],
72266
73386
  ["kiro-cli", {
72267
- class: KiroPermissions,
73387
+ class: KiroScopedPermissions,
72268
73388
  meta: {
72269
73389
  supportsProject: true,
72270
- supportsGlobal: false,
73390
+ supportsGlobal: true,
72271
73391
  supportsImport: true
72272
73392
  }
72273
73393
  }],
72274
73394
  ["kiro-ide", {
72275
- class: KiroPermissions,
73395
+ class: KiroScopedPermissions,
72276
73396
  meta: {
72277
73397
  supportsProject: true,
72278
- supportsGlobal: false,
73398
+ supportsGlobal: true,
72279
73399
  supportsImport: true
72280
73400
  }
72281
73401
  }],
@@ -72526,6 +73646,335 @@ var PermissionsProcessor = class extends FeatureProcessor {
72526
73646
  }
72527
73647
  };
72528
73648
  //#endregion
73649
+ //#region src/types/tool-display.ts
73650
+ const TOOL_DISPLAY = [
73651
+ {
73652
+ key: "agentsmd",
73653
+ label: "AGENTS.md",
73654
+ group: "standard"
73655
+ },
73656
+ {
73657
+ key: "agentsskills",
73658
+ label: "AgentsSkills",
73659
+ group: "standard"
73660
+ },
73661
+ {
73662
+ key: "amp",
73663
+ label: "Amp",
73664
+ group: "ai"
73665
+ },
73666
+ {
73667
+ key: "bob",
73668
+ label: "IBM Bob",
73669
+ group: "ai"
73670
+ },
73671
+ {
73672
+ key: "claudecode",
73673
+ label: "Claude Code",
73674
+ group: "ai"
73675
+ },
73676
+ {
73677
+ key: "claudecode-plugin",
73678
+ label: "Claude Code plugin",
73679
+ group: "ai"
73680
+ },
73681
+ {
73682
+ key: "codebuddy",
73683
+ label: "CodeBuddy Code",
73684
+ group: "ai"
73685
+ },
73686
+ {
73687
+ key: "codewhale",
73688
+ label: "Codewhale",
73689
+ group: "ai"
73690
+ },
73691
+ {
73692
+ key: "codexcli",
73693
+ label: "Codex CLI",
73694
+ group: "ai"
73695
+ },
73696
+ {
73697
+ key: "commandcode",
73698
+ label: "Command Code",
73699
+ group: "ai"
73700
+ },
73701
+ {
73702
+ key: "copilot",
73703
+ label: "GitHub Copilot",
73704
+ group: "ai"
73705
+ },
73706
+ {
73707
+ key: "copilotcli",
73708
+ label: "GitHub Copilot CLI",
73709
+ group: "ai"
73710
+ },
73711
+ {
73712
+ key: "crush",
73713
+ label: "Crush",
73714
+ group: "ai"
73715
+ },
73716
+ {
73717
+ key: "goose",
73718
+ label: "Goose",
73719
+ group: "ai"
73720
+ },
73721
+ {
73722
+ key: "hermesagent",
73723
+ label: "Hermes Agent",
73724
+ group: "ai"
73725
+ },
73726
+ {
73727
+ key: "grokcli",
73728
+ label: "Grok CLI",
73729
+ group: "ai"
73730
+ },
73731
+ {
73732
+ key: "cursor",
73733
+ label: "Cursor",
73734
+ group: "ai"
73735
+ },
73736
+ {
73737
+ key: "deepagents",
73738
+ label: "deepagents-cli",
73739
+ group: "ai"
73740
+ },
73741
+ {
73742
+ key: "factorydroid",
73743
+ label: "Factory Droid",
73744
+ group: "ai"
73745
+ },
73746
+ {
73747
+ key: "opencode",
73748
+ label: "OpenCode",
73749
+ group: "ai"
73750
+ },
73751
+ {
73752
+ key: "openclaw",
73753
+ label: "OpenClaw",
73754
+ group: "ai"
73755
+ },
73756
+ {
73757
+ key: "mimocode",
73758
+ label: "MiMo Code",
73759
+ group: "ai"
73760
+ },
73761
+ {
73762
+ key: "gitlabduo",
73763
+ label: "GitLab Duo CLI",
73764
+ group: "ai"
73765
+ },
73766
+ {
73767
+ key: "cline",
73768
+ label: "Cline",
73769
+ group: "ai"
73770
+ },
73771
+ {
73772
+ key: "kilo",
73773
+ label: "Kilo Code",
73774
+ group: "ai"
73775
+ },
73776
+ {
73777
+ key: "kimi-code",
73778
+ label: "Kimi Code",
73779
+ group: "ai"
73780
+ },
73781
+ {
73782
+ key: "kimi-code-plugin",
73783
+ label: "Kimi Code plugin",
73784
+ group: "ai"
73785
+ },
73786
+ {
73787
+ key: "roo",
73788
+ label: "Roo Code ⚠️",
73789
+ group: "ai"
73790
+ },
73791
+ {
73792
+ key: "zoocode",
73793
+ label: "Zoo Code",
73794
+ group: "ai"
73795
+ },
73796
+ {
73797
+ key: "rovodev",
73798
+ label: "Rovodev (Atlassian) ⚠️",
73799
+ group: "ai"
73800
+ },
73801
+ {
73802
+ key: "takt",
73803
+ label: "Takt",
73804
+ group: "ai"
73805
+ },
73806
+ {
73807
+ key: "trae",
73808
+ label: "Trae",
73809
+ group: "ai"
73810
+ },
73811
+ {
73812
+ key: "tabnine",
73813
+ label: "Tabnine CLI ⚠️",
73814
+ group: "ai"
73815
+ },
73816
+ {
73817
+ key: "continue",
73818
+ label: "Continue ⚠️",
73819
+ group: "ai"
73820
+ },
73821
+ {
73822
+ key: "cortexcode",
73823
+ label: "Snowflake Cortex Code",
73824
+ group: "ai"
73825
+ },
73826
+ {
73827
+ key: "vibe",
73828
+ label: "Vibe Code",
73829
+ group: "ai"
73830
+ },
73831
+ {
73832
+ key: "vibe-plugin",
73833
+ label: "Vibe Code plugin",
73834
+ group: "ai"
73835
+ },
73836
+ {
73837
+ key: "qoder",
73838
+ label: "Qoder",
73839
+ group: "ai"
73840
+ },
73841
+ {
73842
+ key: "qwencode",
73843
+ label: "Qwen Code",
73844
+ group: "ai"
73845
+ },
73846
+ {
73847
+ key: "musecode",
73848
+ label: "Meta Muse Code",
73849
+ group: "ai"
73850
+ },
73851
+ {
73852
+ key: "reasonix",
73853
+ label: "Reasonix",
73854
+ group: "ai"
73855
+ },
73856
+ {
73857
+ key: "codebuff",
73858
+ label: "Codebuff (Freebuff)",
73859
+ group: "ai"
73860
+ },
73861
+ {
73862
+ key: "kiro",
73863
+ label: "Kiro ⚠️",
73864
+ group: "ai"
73865
+ },
73866
+ {
73867
+ key: "kiro-cli",
73868
+ label: "Kiro CLI",
73869
+ group: "ai"
73870
+ },
73871
+ {
73872
+ key: "kiro-ide",
73873
+ label: "Kiro IDE",
73874
+ group: "ai"
73875
+ },
73876
+ {
73877
+ key: "lettacode",
73878
+ label: "Letta Code",
73879
+ group: "ai"
73880
+ },
73881
+ {
73882
+ key: "antigravity-ide",
73883
+ label: "Google Antigravity IDE",
73884
+ group: "ai"
73885
+ },
73886
+ {
73887
+ key: "antigravity-cli",
73888
+ label: "Google Antigravity CLI",
73889
+ group: "ai"
73890
+ },
73891
+ {
73892
+ key: "antigravity-plugin",
73893
+ label: "Google Antigravity plugin",
73894
+ group: "ai"
73895
+ },
73896
+ {
73897
+ key: "aiassistant",
73898
+ label: "JetBrains AI Assistant",
73899
+ group: "ai"
73900
+ },
73901
+ {
73902
+ key: "junie",
73903
+ label: "JetBrains Junie",
73904
+ group: "ai"
73905
+ },
73906
+ {
73907
+ key: "augmentcode",
73908
+ label: "AugmentCode",
73909
+ group: "ai"
73910
+ },
73911
+ {
73912
+ key: "augmentcode-plugin",
73913
+ label: "AugmentCode plugin",
73914
+ group: "ai"
73915
+ },
73916
+ {
73917
+ key: "devin",
73918
+ label: "Devin Desktop",
73919
+ group: "ai"
73920
+ },
73921
+ {
73922
+ key: "devin-plugin",
73923
+ label: "Devin plugin",
73924
+ group: "ai"
73925
+ },
73926
+ {
73927
+ key: "warp",
73928
+ label: "Warp",
73929
+ group: "ai"
73930
+ },
73931
+ {
73932
+ key: "warpcli",
73933
+ label: "Warp Agent CLI",
73934
+ group: "ai"
73935
+ },
73936
+ {
73937
+ key: "replit",
73938
+ label: "Replit",
73939
+ group: "ai"
73940
+ },
73941
+ {
73942
+ key: "pi",
73943
+ label: "Pi Coding Agent",
73944
+ group: "ai"
73945
+ },
73946
+ {
73947
+ key: "omp",
73948
+ label: "oh-my-pi",
73949
+ group: "ai"
73950
+ },
73951
+ {
73952
+ key: "zed",
73953
+ label: "Zed",
73954
+ group: "ai"
73955
+ },
73956
+ {
73957
+ key: "zcode",
73958
+ label: "ZCode (Z.ai)",
73959
+ group: "ai"
73960
+ },
73961
+ {
73962
+ key: "zcode-plugin",
73963
+ label: "ZCode plugin",
73964
+ group: "ai"
73965
+ },
73966
+ {
73967
+ key: "pool",
73968
+ label: "Pool (Poolside)",
73969
+ group: "ai"
73970
+ },
73971
+ {
73972
+ key: "dsh",
73973
+ label: "DeepSeek Harness",
73974
+ group: "ai"
73975
+ }
73976
+ ];
73977
+ //#endregion
72529
73978
  //#region src/features/skills/simulated-skill.ts
72530
73979
  const SimulatedSkillFrontmatterSchema = zod_mini.z.looseObject({
72531
73980
  name: zod_mini.z.string(),
@@ -77933,6 +79382,24 @@ var KimiCodeSkill = class extends ToolSkill {
77933
79382
  const targets = rulesyncSkill.getFrontmatter().targets;
77934
79383
  return targets.includes("*") || targets.includes("kimi-code");
77935
79384
  }
79385
+ /**
79386
+ * Commands are emitted into the Kimi-specific skills tree as
79387
+ * `<slug>/SKILL.md` (see `KimiCodeCommand`), so a directory there matching a
79388
+ * current rulesync command slug (of a command that targets `kimi-code`) is
79389
+ * owned by the commands feature: it must not be imported as a skill nor
79390
+ * deleted as an orphan skill. A command aimed only at other tools never
79391
+ * reaches this tree, so it leaves a same-named skill alone. The import-only
79392
+ * `.agents/skills/` root never receives Kimi Code commands, so a same-named
79393
+ * skill in it stays a skill.
79394
+ */
79395
+ static async isDirOwned({ relativeDirPath, dirName, inputRoots, global }) {
79396
+ if (relativeDirPath !== this.getSettablePaths({ global }).relativeDirPath) return true;
79397
+ return !await rulesyncCommandSlugExists({
79398
+ inputRoots,
79399
+ dirName,
79400
+ toolTarget: "kimi-code"
79401
+ });
79402
+ }
77936
79403
  static async fromDir(params) {
77937
79404
  const loaded = await this.loadSkillDirContent({
77938
79405
  ...params,
@@ -78006,6 +79473,14 @@ var KimiCodePluginSkill = class extends KimiCodeSkill {
78006
79473
  return { relativeDirPath: KIMI_CODE_PLUGIN_SKILLS_DIR };
78007
79474
  }
78008
79475
  /**
79476
+ * Unlike `.kimi-code/skills/`, the plugin `skills/` tree is not shared with
79477
+ * the commands feature (`kimi-code-plugin` writes commands to `commands/`),
79478
+ * so every directory in it is a skill.
79479
+ */
79480
+ static async isDirOwned() {
79481
+ return true;
79482
+ }
79483
+ /**
78009
79484
  * The bundle is read from `--output-root <plugin>`, but the imported rulesync
78010
79485
  * file belongs to the project's `.rulesync/`, not to the plugin root.
78011
79486
  */
@@ -78357,7 +79832,8 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
78357
79832
  license: zod_mini.z.optional(zod_mini.z.unknown()),
78358
79833
  compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
78359
79834
  metadata: zod_mini.z.optional(zod_mini.z.unknown()),
78360
- "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
79835
+ "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
79836
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
78361
79837
  });
78362
79838
  var OpenCodeSkill = class extends ToolSkill {
78363
79839
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
@@ -78474,7 +79950,8 @@ var OpenCodeSkill = class extends ToolSkill {
78474
79950
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
78475
79951
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
78476
79952
  ...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
78477
- ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
79953
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata },
79954
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
78478
79955
  };
78479
79956
  const { toolTarget } = this.constructor.layout;
78480
79957
  const rulesyncFrontmatter = {
@@ -78509,13 +79986,18 @@ var OpenCodeSkill = class extends ToolSkill {
78509
79986
  rootFrontmatter: rulesyncFrontmatter,
78510
79987
  section: opencodeSection
78511
79988
  });
79989
+ const disableModelInvocation = resolveDisableModelInvocation({
79990
+ rootFrontmatter: rulesyncFrontmatter,
79991
+ section: opencodeSection
79992
+ });
78512
79993
  const opencodeFrontmatter = {
78513
79994
  name: rulesyncFrontmatter.name,
78514
79995
  description: rulesyncFrontmatter.description,
78515
79996
  ...license !== void 0 && { license },
78516
79997
  ...compatibility !== void 0 && { compatibility },
78517
79998
  ...metadata !== void 0 && { metadata },
78518
- ...opencodeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": opencodeSection["allowed-tools"] }
79999
+ ...opencodeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": opencodeSection["allowed-tools"] },
80000
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation }
78519
80001
  };
78520
80002
  const settablePaths = this.getSettablePaths({ global });
78521
80003
  return new this({
@@ -78538,11 +80020,13 @@ var OpenCodeSkill = class extends ToolSkill {
78538
80020
  ...params,
78539
80021
  getSettablePaths: (options) => this.getSettablePaths(options)
78540
80022
  });
78541
- const result = OpenCodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
78542
- if (!result.success) {
78543
- const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
78544
- throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
78545
- }
80023
+ const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
80024
+ const frontmatter = normalizeDisableModelInvocation({
80025
+ frontmatter: loaded.frontmatter,
80026
+ skillFilePath: (0, node_path.join)(skillDirPath, SKILL_FILE_NAME)
80027
+ });
80028
+ const result = OpenCodeSkillFrontmatterSchema.safeParse(frontmatter);
80029
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
78546
80030
  return new this({
78547
80031
  outputRoot: loaded.outputRoot,
78548
80032
  relativeDirPath: loaded.relativeDirPath,
@@ -78570,6 +80054,52 @@ var OpenCodeSkill = class extends ToolSkill {
78570
80054
  });
78571
80055
  }
78572
80056
  };
80057
+ const DISABLE_MODEL_INVOCATION_KEY = "disable-model-invocation";
80058
+ /**
80059
+ * Reads a `disable-model-invocation` value the way OpenCode V2 does
80060
+ * (`booleanValue` in `packages/core/src/config/plugin/skill-file.ts`): a
80061
+ * boolean, the numbers `1` / `0`, or a `true`/`yes`/`on`/`1` /
80062
+ * `false`/`no`/`off`/`0` string compared after trimming and case-folding.
80063
+ * Anything else is `undefined`, which OpenCode ignores.
80064
+ */
80065
+ function parseOpencodeBoolean(value) {
80066
+ if (typeof value === "boolean") return value;
80067
+ if (typeof value === "number") return value === 1 ? true : value === 0 ? false : void 0;
80068
+ if (typeof value !== "string") return;
80069
+ const normalized = value.trim().toLowerCase();
80070
+ if ([
80071
+ "true",
80072
+ "yes",
80073
+ "on",
80074
+ "1"
80075
+ ].includes(normalized)) return true;
80076
+ if ([
80077
+ "false",
80078
+ "no",
80079
+ "off",
80080
+ "0"
80081
+ ].includes(normalized)) return false;
80082
+ }
80083
+ /**
80084
+ * Coerces an imported `disable-model-invocation` onto the boolean the schema
80085
+ * types. A spelling OpenCode cannot read is dropped with a warning instead of
80086
+ * failing the import, since OpenCode itself ignores it.
80087
+ */
80088
+ function normalizeDisableModelInvocation({ frontmatter, skillFilePath }) {
80089
+ if (!Object.hasOwn(frontmatter, DISABLE_MODEL_INVOCATION_KEY)) return frontmatter;
80090
+ const raw = frontmatter[DISABLE_MODEL_INVOCATION_KEY];
80091
+ const parsed = parseOpencodeBoolean(raw);
80092
+ if (parsed === raw) return frontmatter;
80093
+ const { [DISABLE_MODEL_INVOCATION_KEY]: _dropped, ...rest } = frontmatter;
80094
+ if (parsed === void 0) {
80095
+ warnOnceWithFallback(void 0, `${stripControlCharacters(toPosixPath(skillFilePath))}: ignoring unreadable "${DISABLE_MODEL_INVOCATION_KEY}" value ${stripControlCharacters(JSON.stringify(raw) ?? String(raw))}; OpenCode ignores it too.`);
80096
+ return rest;
80097
+ }
80098
+ return {
80099
+ ...rest,
80100
+ [DISABLE_MODEL_INVOCATION_KEY]: parsed
80101
+ };
80102
+ }
78573
80103
  //#endregion
78574
80104
  //#region src/features/skills/mimocode-skill.ts
78575
80105
  /**
@@ -84871,7 +86401,7 @@ var DeepagentsSubagent = class DeepagentsSubagent extends ToolSubagent {
84871
86401
  //#endregion
84872
86402
  //#region src/features/subagents/devin-subagent.ts
84873
86403
  const DevinSubagentFrontmatterSchema = zod_mini.z.looseObject({
84874
- name: zod_mini.z.string(),
86404
+ name: zod_mini.z.optional(zod_mini.z.string()),
84875
86405
  description: zod_mini.z.optional(zod_mini.z.string()),
84876
86406
  model: zod_mini.z.optional(zod_mini.z.string()),
84877
86407
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -84924,7 +86454,7 @@ var DevinSubagent = class extends ToolSubagent {
84924
86454
  outputRoot: ".",
84925
86455
  frontmatter: {
84926
86456
  targets: ["*"],
84927
- name,
86457
+ name: name ?? this.getSubagentName(),
84928
86458
  description,
84929
86459
  ...Object.keys(devinSection).length > 0 && { devin: devinSection }
84930
86460
  },
@@ -84998,7 +86528,8 @@ var DevinSubagent = class extends ToolSubagent {
84998
86528
  const paths = this.getSettablePaths({ global });
84999
86529
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
85000
86530
  const fileContent = await readFileContent(filePath);
85001
- const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
86531
+ const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
86532
+ if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}`);
85002
86533
  const result = DevinSubagentFrontmatterSchema.safeParse(frontmatter);
85003
86534
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
85004
86535
  return new this({
@@ -98646,6 +100177,7 @@ var RulesProcessor = class extends FeatureProcessor {
98646
100177
  warnForOutputStyleRuleOptions({ rules, factory }) {
98647
100178
  const toolTarget = this.toolTarget;
98648
100179
  if (toolTarget !== "factorydroid" && toolTarget !== "qoder") return;
100180
+ const toolLabel = TOOL_DISPLAY.find((entry) => entry.key === toolTarget)?.label ?? toolTarget;
98649
100181
  for (const rule of rules) {
98650
100182
  const frontmatter = rule.getFrontmatter();
98651
100183
  const section = frontmatter[toolTarget];
@@ -98653,7 +100185,7 @@ var RulesProcessor = class extends FeatureProcessor {
98653
100185
  const { channel, name } = section;
98654
100186
  const fileName = stripControlCharacters(rule.getRelativeFilePath());
98655
100187
  const targets = frontmatter.targets ?? ["*"];
98656
- if (channel === "output-style" && targets.some((target) => target !== toolTarget)) this.logger.warn(`${fileName} sets ${toolTarget}.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["${toolTarget}"]\` to keep the output style to ${toolTarget === "qoder" ? "Qoder" : "Factory Droid"}.`);
100188
+ if (channel === "output-style" && targets.some((target) => target !== toolTarget)) this.logger.warn(`${fileName} sets ${toolTarget}.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["${toolTarget}"]\` to keep the output style to ${toolLabel}.`);
98657
100189
  if (channel !== "output-style" && name !== void 0) this.logger.warn(`${fileName} sets ${toolTarget}.name, which is read only with ${toolTarget}.channel: output-style; ignoring it.`);
98658
100190
  }
98659
100191
  }
@@ -100073,13 +101605,15 @@ const ANTIGRAVITY_PLUGIN_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/;
100073
101605
  * The manifest declares only `name` and `description`, and rejects anything
100074
101606
  * else, so the one required field is derived from the plugin directory's name.
100075
101607
  * An existing manifest is never read or rewritten: it may carry a
100076
- * hand-authored `name` or `description` that rulesync has no source for.
101608
+ * hand-authored `name` or `description` that rulesync has no source for. A
101609
+ * manifest kept in one of the manifest folders counts as present too, so no
101610
+ * root stub is written next to it.
100077
101611
  */
100078
101612
  async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger }) {
100079
101613
  const paths = [];
100080
101614
  for (const outputRoot of outputRoots) {
100081
101615
  const filePath = (0, node_path.join)(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME);
100082
- if (await fileExists(filePath)) continue;
101616
+ if (await hasAntigravityPluginManifest(outputRoot)) continue;
100083
101617
  if (await refusesWriteOutsideRoot({
100084
101618
  logger,
100085
101619
  rootPath: outputRoot,
@@ -100102,6 +101636,11 @@ async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger })
100102
101636
  sourceLoadFailed: false
100103
101637
  };
100104
101638
  }
101639
+ async function hasAntigravityPluginManifest(outputRoot) {
101640
+ const candidates = [(0, node_path.join)(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME), ...ANTIGRAVITY_PLUGIN_MANIFEST_FOLDERS.map((folder) => (0, node_path.join)(outputRoot, folder, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME))];
101641
+ for (const candidate of candidates) if (await fileExists(candidate)) return true;
101642
+ return false;
101643
+ }
100105
101644
  //#endregion
100106
101645
  //#region src/features/shared/hermes-project-plugin-activation.ts
100107
101646
  function mergeEnabledPlugins({ existingContent, filePath, pluginNames }) {
@@ -103053,10 +104592,10 @@ Object.defineProperty(exports, "generate", {
103053
104592
  return generate;
103054
104593
  }
103055
104594
  });
103056
- Object.defineProperty(exports, "getFileSize", {
104595
+ Object.defineProperty(exports, "getFileSizeAndExecutableBit", {
103057
104596
  enumerable: true,
103058
104597
  get: function() {
103059
- return getFileSize;
104598
+ return getFileSizeAndExecutableBit;
103060
104599
  }
103061
104600
  });
103062
104601
  Object.defineProperty(exports, "getHomeDirectory", {
@@ -103101,12 +104640,6 @@ Object.defineProperty(exports, "hasEnclosingMarkOutsideKeycap", {
103101
104640
  return hasEnclosingMarkOutsideKeycap;
103102
104641
  }
103103
104642
  });
103104
- Object.defineProperty(exports, "hasExecutableBit", {
103105
- enumerable: true,
103106
- get: function() {
103107
- return hasExecutableBit;
103108
- }
103109
- });
103110
104643
  Object.defineProperty(exports, "importFromTool", {
103111
104644
  enumerable: true,
103112
104645
  get: function() {
@@ -103167,6 +104700,12 @@ Object.defineProperty(exports, "loadYaml", {
103167
104700
  return loadYaml;
103168
104701
  }
103169
104702
  });
104703
+ Object.defineProperty(exports, "mapWithConcurrency", {
104704
+ enumerable: true,
104705
+ get: function() {
104706
+ return mapWithConcurrency;
104707
+ }
104708
+ });
103170
104709
  Object.defineProperty(exports, "mergeInputRootConfigs", {
103171
104710
  enumerable: true,
103172
104711
  get: function() {
@@ -103191,6 +104730,12 @@ Object.defineProperty(exports, "pathEscapesRoot", {
103191
104730
  return pathEscapesRoot;
103192
104731
  }
103193
104732
  });
104733
+ Object.defineProperty(exports, "posixRelativePathEscapesRoot", {
104734
+ enumerable: true,
104735
+ get: function() {
104736
+ return posixRelativePathEscapesRoot;
104737
+ }
104738
+ });
103194
104739
  Object.defineProperty(exports, "quoteForLog", {
103195
104740
  enumerable: true,
103196
104741
  get: function() {
@@ -103263,10 +104808,10 @@ Object.defineProperty(exports, "resolveRulesyncSourceWritePath", {
103263
104808
  return resolveRulesyncSourceWritePath;
103264
104809
  }
103265
104810
  });
103266
- Object.defineProperty(exports, "resolvedPathEscapesRoot", {
104811
+ Object.defineProperty(exports, "resolvedRelativePath", {
103267
104812
  enumerable: true,
103268
104813
  get: function() {
103269
- return resolvedPathEscapesRoot;
104814
+ return resolvedRelativePath;
103270
104815
  }
103271
104816
  });
103272
104817
  Object.defineProperty(exports, "runWithDirectoryRollback", {