rulesync 35.0.0 → 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$420 } = 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$420(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$420(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$420(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$420(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$420(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$420(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$420(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$420(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";
@@ -699,6 +699,7 @@ const commandsProcessorToolTargetTuple = [
699
699
  "hermesagent",
700
700
  "junie",
701
701
  "kilo",
702
+ "kimi-code",
702
703
  "kimi-code-plugin",
703
704
  "kiro",
704
705
  "kiro-cli",
@@ -8173,7 +8174,8 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8173
8174
  * autonomy/sandbox controls with no canonical permission category — under
8174
8175
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
8175
8176
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
8176
- * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
8177
+ * `todoWrite`, `codeModeOnly`, `freeform`, `workflowsEnabled`, `workflowNameOnly`,
8178
+ * `executionSandbox`) and
8177
8179
  * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
8178
8180
  * `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
8179
8181
  * `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
@@ -8192,7 +8194,8 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8192
8194
  * changes what the file said is reported in either scope, naming what that key
8193
8195
  * decides there — the autonomy and containment
8194
8196
  * controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
8195
- * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
8197
+ * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`,
8198
+ * `codeModeOnly`, `freeform`, and `eager`, which
8196
8199
  * demotes an omitted tool to deferred rather than removing it, replacing rather
8197
8200
  * than merging with the list a higher scope set), the Auto Mode
8198
8201
  * classifier config, the skill kill-switches, and, because these groups are
@@ -10885,13 +10888,15 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
10885
10888
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
10886
10889
  license: zod_mini.z.optional(zod_mini.z.unknown()),
10887
10890
  compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
10888
- 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())
10889
10893
  })),
10890
10894
  mimocode: zod_mini.z.optional(zod_mini.z.looseObject({
10891
10895
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
10892
10896
  license: zod_mini.z.optional(zod_mini.z.unknown()),
10893
10897
  compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
10894
- 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())
10895
10900
  })),
10896
10901
  kilo: zod_mini.z.optional(zod_mini.z.looseObject({
10897
10902
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -11344,7 +11349,7 @@ async function getLocalSkillDirNames(sourceTree) {
11344
11349
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
11345
11350
  * default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11346
11351
  * copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
11347
- * commandcode, lettacode). Each tool's own section may override that
11352
+ * commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
11348
11353
  * default with a per-target value. A defined section value (including `false`)
11349
11354
  * always wins over the root default.
11350
11355
  *
@@ -13543,8 +13548,8 @@ function diffSharedConfigOwnedKeys({ fileKey, existingContent, newContent }) {
13543
13548
  return operations;
13544
13549
  }
13545
13550
  const READ_TOOL_NAME = "Read";
13546
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
13547
- 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})`;
13548
13553
  const parsePermissionsBlock = (settings) => {
13549
13554
  const permissions = settings.permissions ?? {};
13550
13555
  return {
@@ -13568,9 +13573,9 @@ const withPermissions = (settings, next) => {
13568
13573
  };
13569
13574
  };
13570
13575
  const applyIgnoreReadDenies = (params) => {
13571
- const { settings, readDenies } = params;
13576
+ const { settings, readDenies, readToolName = READ_TOOL_NAME } = params;
13572
13577
  const current = parsePermissionsBlock(settings);
13573
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
13578
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry, readToolName) || readDenies.includes(entry));
13574
13579
  return withPermissions(settings, {
13575
13580
  allow: current.allow,
13576
13581
  ask: current.ask,
@@ -17897,6 +17902,7 @@ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
17897
17902
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
17898
17903
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
17899
17904
  const ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME = "plugin.json";
17905
+ const ANTIGRAVITY_PLUGIN_MANIFEST_FOLDERS = [".antigravity-plugin", ".gemini-plugin"];
17900
17906
  const AUGMENTCODE_PLUGIN_RULES_DIR = "rules";
17901
17907
  const AUGMENTCODE_PLUGIN_COMMANDS_DIR = "commands";
17902
17908
  const AUGMENTCODE_PLUGIN_AGENTS_DIR = "agents";
@@ -19543,9 +19549,36 @@ function commandSlug(relativeFilePath) {
19543
19549
  * skill. Once the command is removed from every source tree's `commands/`
19544
19550
  * directory, the directory stops matching and the skills feature cleans
19545
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.
19546
19557
  */
19547
- async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
19548
- 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;
19549
19582
  }
19550
19583
  //#endregion
19551
19584
  //#region src/features/commands/devin-command.ts
@@ -19563,7 +19596,7 @@ const DEVIN_COMMAND_SECTION_KEYS = [
19563
19596
  "permissions",
19564
19597
  "triggers"
19565
19598
  ];
19566
- function commandSkillContent$1(rulesyncCommand) {
19599
+ function commandSkillContent$2(rulesyncCommand) {
19567
19600
  const slug = commandSlug(rulesyncCommand.getRelativeFilePath());
19568
19601
  const frontmatter = rulesyncCommand.getFrontmatter();
19569
19602
  const description = frontmatter.description ?? `${slug} command`;
@@ -19637,7 +19670,7 @@ var DevinCommand = class DevinCommand extends ToolCommand {
19637
19670
  relativeDirPath: paths.relativeDirPath,
19638
19671
  relativeFilePath: SKILL_FILE_NAME,
19639
19672
  slug: commandSlug(rulesyncCommand.getRelativeFilePath()),
19640
- fileContent: commandSkillContent$1(rulesyncCommand)
19673
+ fileContent: commandSkillContent$2(rulesyncCommand)
19641
19674
  });
19642
19675
  }
19643
19676
  getFileContent() {
@@ -21457,6 +21490,180 @@ var KiloCommand = class KiloCommand extends ToolCommand {
21457
21490
  }
21458
21491
  };
21459
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
21460
21667
  //#region src/features/commands/kimi-code-plugin-command.ts
21461
21668
  /**
21462
21669
  * Kimi Code reads only `name` and `description` from a plugin command. The
@@ -21611,6 +21818,14 @@ const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
21611
21818
  const KIRO_MCP_FILE_NAME = "mcp.json";
21612
21819
  const KIRO_IGNORE_FILE_NAME = ".kiroignore";
21613
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";
21614
21829
  //#endregion
21615
21830
  //#region src/features/commands/kiro-command.ts
21616
21831
  var KiroCommand = class KiroCommand extends ToolCommand {
@@ -23653,6 +23868,11 @@ const TraeCommandFrontmatterSchema = zod_mini.z.looseObject({
23653
23868
  name: zod_mini.z.optional(zod_mini.z.string())
23654
23869
  });
23655
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
+ /**
23656
23876
  * Custom command for Trae.
23657
23877
  *
23658
23878
  * Trae reads Markdown command files under `.trae/commands/` (project, up to
@@ -23712,10 +23932,11 @@ var TraeCommand = class TraeCommand extends ToolCommand {
23712
23932
  }
23713
23933
  static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
23714
23934
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
23935
+ const { name: _name, ...traeFields } = rulesyncFrontmatter.trae ?? {};
23715
23936
  const traeFrontmatter = {
23716
- name: (0, node_path.basename)(rulesyncCommand.getRelativeFilePath(), ".md"),
23937
+ name: this.deriveName(rulesyncCommand),
23717
23938
  description: rulesyncFrontmatter.description,
23718
- ...rulesyncFrontmatter.trae
23939
+ ...traeFields
23719
23940
  };
23720
23941
  return new TraeCommand({
23721
23942
  outputRoot,
@@ -23741,6 +23962,36 @@ var TraeCommand = class TraeCommand extends ToolCommand {
23741
23962
  error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
23742
23963
  };
23743
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
+ }
23744
23995
  static isTargetedByRulesyncCommand(rulesyncCommand) {
23745
23996
  return this.isTargetedByRulesyncCommandDefault({
23746
23997
  rulesyncCommand,
@@ -24324,6 +24575,17 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24324
24575
  supportsSubdirectory: false
24325
24576
  }
24326
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
+ }],
24327
24589
  ["kimi-code-plugin", {
24328
24590
  class: KimiCodePluginCommand,
24329
24591
  meta: {
@@ -24577,7 +24839,8 @@ var CommandsProcessor = class extends FeatureProcessor {
24577
24839
  await factory.class.validateRulesyncCommands?.({
24578
24840
  inputRoots: this.inputRoots,
24579
24841
  rulesyncCommands,
24580
- logger: this.logger
24842
+ logger: this.logger,
24843
+ global: this.global
24581
24844
  });
24582
24845
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
24583
24846
  const toolCommands = [];
@@ -26257,8 +26520,8 @@ function isSupportedHookType({ type, converterConfig }) {
26257
26520
  * side either way, so the value is dropped rather than carried into a config
26258
26521
  * the tool would ignore or refuse.
26259
26522
  */
26260
- function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }) {
26261
- 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;
26262
26525
  const field = direction === "generated" ? canonical : tool;
26263
26526
  if (!isFieldApplicable({
26264
26527
  commandOnly,
@@ -26267,10 +26530,23 @@ function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }
26267
26530
  warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not ${direction}.`);
26268
26531
  return false;
26269
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
+ }
26270
26537
  if (events !== void 0 && !events.has(eventName)) {
26271
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}.`);
26272
26539
  return false;
26273
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
+ }
26274
26550
  return true;
26275
26551
  }
26276
26552
  /**
@@ -26287,6 +26563,7 @@ function emitKeywordFields({ def, hookType, eventName, fields, warn }) {
26287
26563
  spec,
26288
26564
  hookType,
26289
26565
  eventName,
26566
+ flags: def,
26290
26567
  direction: "generated",
26291
26568
  warn
26292
26569
  })) emitted[spec.tool] = spec.trueValue;
@@ -26308,6 +26585,7 @@ function importKeywordFields({ h, hookType, eventName, fields, warn }) {
26308
26585
  spec,
26309
26586
  hookType,
26310
26587
  eventName,
26588
+ flags: h,
26311
26589
  direction: "imported",
26312
26590
  warn
26313
26591
  })) continue;
@@ -28200,6 +28478,20 @@ const CLAUDE_SPEC = {
28200
28478
  tool: "continueOnBlock"
28201
28479
  }
28202
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
+ }],
28203
28495
  arrayPassthroughFields: [{
28204
28496
  canonical: "args",
28205
28497
  tool: "args",
@@ -28922,18 +29214,20 @@ const CODEXCLI_CONVERTER_CONFIG = {
28922
29214
  canonicalToToolEventNames: CANONICAL_TO_CODEXCLI_EVENT_NAMES,
28923
29215
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
28924
29216
  projectDirVar: "",
28925
- supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
29217
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "mcp_tool"]),
28926
29218
  passthroughFields: ["name", "description"],
28927
29219
  stringPassthroughFields: [{
28928
29220
  canonical: "commandWindows",
28929
- tool: "commandWindows"
29221
+ tool: "commandWindows",
29222
+ commandOnly: true
28930
29223
  }, {
28931
29224
  canonical: "statusMessage",
28932
29225
  tool: "statusMessage"
28933
29226
  }],
28934
29227
  numberPassthroughFields: [{
28935
29228
  canonical: "additionalContextLimit",
28936
- tool: "additionalContextLimit"
29229
+ tool: "additionalContextLimit",
29230
+ commandOnly: true
28937
29231
  }],
28938
29232
  booleanPassthroughFields: [{
28939
29233
  canonical: "async",
@@ -28942,6 +29236,74 @@ const CODEXCLI_CONVERTER_CONFIG = {
28942
29236
  }]
28943
29237
  };
28944
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
+ /**
28945
29307
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
28946
29308
  * Reads the existing file (if any), parses TOML, and returns the content without writing to
28947
29309
  * disk. The caller is responsible for writing via the normal write phase.
@@ -29034,10 +29396,13 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
29034
29396
  const config = rulesyncHooks.getJson();
29035
29397
  const merged = mergeGeneratedHookLists({
29036
29398
  existingContent,
29037
- generatedHooks: canonicalToToolHooks({
29038
- config,
29039
- toolOverrideHooks: config.codexcli?.hooks,
29040
- 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
+ }),
29041
29406
  logger
29042
29407
  }),
29043
29408
  shape: "matcher-groups",
@@ -32216,60 +32581,6 @@ var KiloHooks = class KiloHooks extends ToolHooks {
32216
32581
  }
32217
32582
  };
32218
32583
  //#endregion
32219
- //#region src/constants/kimi-code-paths.ts
32220
- const KIMI_CODE_DIR = ".kimi-code";
32221
- const KIMI_CODE_RULE_FILE_NAME = "AGENTS.md";
32222
- const KIMI_CODE_MCP_FILE_NAME = "mcp.json";
32223
- const KIMI_CODE_CONFIG_FILE_NAME = "config.toml";
32224
- const KIMI_CODE_SKILLS_DIR_NAME = "skills";
32225
- const KIMI_CODE_AGENTS_DIR_NAME = "agents";
32226
- const KIMI_CODE_SKILLS_DIR_PATH = (0, node_path.join)(KIMI_CODE_DIR, KIMI_CODE_SKILLS_DIR_NAME);
32227
- (0, node_path.join)(KIMI_CODE_DIR, KIMI_CODE_AGENTS_DIR_NAME);
32228
- const KIMI_CODE_SHARED_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
32229
- const KIMI_CODE_SHARED_AGENTS_DIR_PATH = (0, node_path.join)(".agents", "agents");
32230
- //#endregion
32231
- //#region src/utils/kimi-code.ts
32232
- function getKimiCodeHome() {
32233
- const configuredHome = process.env.KIMI_CODE_HOME?.trim();
32234
- return configuredHome ? (0, node_path.resolve)(configuredHome) : void 0;
32235
- }
32236
- function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
32237
- return global && getKimiCodeHome() ? relativeDirPath : (0, node_path.join)(KIMI_CODE_DIR, relativeDirPath);
32238
- }
32239
- /**
32240
- * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
32241
- * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
32242
- * Declared unconditionally so the derived shared-file keys — and the drift
32243
- * guards checked against them — do not depend on the ambient environment.
32244
- */
32245
- function getKimiCodeSharedConfigWritePaths() {
32246
- return [{
32247
- relativeDirPath: KIMI_CODE_DIR,
32248
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32249
- }, {
32250
- relativeDirPath: ".",
32251
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32252
- }];
32253
- }
32254
- /**
32255
- * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
32256
- * Both spellings carry the same declaration, but passing the key of the file
32257
- * being written keeps the write path and the drift guards on the same entry.
32258
- */
32259
- function getKimiCodeConfigSharedFileKey({ global }) {
32260
- return sharedConfigFileKey({
32261
- relativeDirPath: getKimiCodeRelativeDirPath({ global }),
32262
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32263
- });
32264
- }
32265
- function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
32266
- return getToolRulesyncOutputRoot({
32267
- nativeOutputRoot,
32268
- global,
32269
- toolHome: getKimiCodeHome
32270
- });
32271
- }
32272
- //#endregion
32273
32584
  //#region src/features/hooks/kimi-code-hooks.ts
32274
32585
  function runFromTrustedDirectory({ command, trustedDirectory }) {
32275
32586
  if (process.platform === "win32") return `set "RULESYNC_KIMI_HOOK_CWD=1" && cd /d "${trustedDirectory.replaceAll("%", "%%").replaceAll("\"", "\"\"")}" && ${command}`;
@@ -35726,7 +36037,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
35726
36037
  supportsImport: true
35727
36038
  },
35728
36039
  supportedEvents: CODEXCLI_HOOK_EVENTS,
35729
- supportedHookTypes: ["command"],
36040
+ supportedHookTypes: ["command", "mcp_tool"],
35730
36041
  supportsMatcher: true
35731
36042
  }],
35732
36043
  ["commandcode", {
@@ -37988,6 +38299,23 @@ function toReasonixTable(value) {
37988
38299
  if (!isPlainObject$1(value)) return {};
37989
38300
  return { ...value };
37990
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";
37991
38319
  //#endregion
37992
38320
  //#region src/features/ignore/reasonix-ignore.ts
37993
38321
  const permissionsTableOf = (document) => toReasonixTable(document.permissions);
@@ -38006,10 +38334,15 @@ const asClaudeStyleSettings = (document) => {
38006
38334
  ...table,
38007
38335
  allow: toReasonixStringArray(table.allow),
38008
38336
  ask: toReasonixStringArray(table.ask),
38009
- deny: toReasonixStringArray(table.deny)
38337
+ deny: toReasonixStringArray(table.deny).filter((entry) => !isReadDenyEntry(entry, REASONIX_LEGACY_READ_TOOL_NAME))
38010
38338
  }
38011
38339
  };
38012
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
+ };
38013
38346
  /**
38014
38347
  * Drop a `[permissions]` table that ended up with nothing in it, so an empty
38015
38348
  * `.rulesyncignore` does not add a bare table header to a file that never had
@@ -38022,13 +38355,16 @@ const withoutEmptyPermissions = (settings) => {
38022
38355
  return document;
38023
38356
  };
38024
38357
  /**
38025
- * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
38358
+ * Writes `.rulesyncignore` patterns as `read_file(<pattern>)` entries in the
38026
38359
  * `[permissions] deny` table of `reasonix.toml` (project) /
38027
38360
  * `~/.reasonix/config.toml` (global).
38028
38361
  *
38029
38362
  * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
38030
38363
  * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
38031
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.
38032
38368
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
38033
38369
  */
38034
38370
  var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
@@ -38055,7 +38391,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
38055
38391
  return false;
38056
38392
  }
38057
38393
  toRulesyncIgnore() {
38058
- 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));
38059
38395
  return new RulesyncIgnore({
38060
38396
  outputRoot: this.outputRoot,
38061
38397
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
@@ -38064,7 +38400,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
38064
38400
  });
38065
38401
  }
38066
38402
  static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
38067
- 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));
38068
38404
  const paths = this.getSettablePaths({ global });
38069
38405
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
38070
38406
  const existingDocument = parseSharedConfig({
@@ -38074,7 +38410,8 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
38074
38410
  });
38075
38411
  const document = withoutEmptyPermissions(applyIgnoreReadDenies({
38076
38412
  settings: asClaudeStyleSettings(existingDocument),
38077
- readDenies
38413
+ readDenies,
38414
+ readToolName: REASONIX_READ_TOOL_NAME
38078
38415
  }));
38079
38416
  return new ReasonixIgnore({
38080
38417
  outputRoot,
@@ -45577,7 +45914,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
45577
45914
  feature: "mcp",
45578
45915
  existingContent: fileContent ?? "",
45579
45916
  patch: {
45580
- mcp: convertedMcp,
45917
+ mcp: undefinedWhenEmpty(convertedMcp),
45581
45918
  ...buildKiloMcpPermissionPatch({
45582
45919
  existing: existingDocument,
45583
45920
  managedServerNames: (0, es_toolkit.uniq)([...Object.keys(mcpServers), ...Object.keys(isRecord$1(existingDocument.mcp) ? existingDocument.mcp : {})]),
@@ -46437,7 +46774,7 @@ var OpencodeMcp = class extends ToolMcp {
46437
46774
  feature: "mcp",
46438
46775
  existingContent: fileContent ?? "",
46439
46776
  patch: {
46440
- mcp: convertedMcp,
46777
+ mcp: undefinedWhenEmpty(convertedMcp),
46441
46778
  tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
46442
46779
  },
46443
46780
  filePath: (0, node_path.join)(jsonDir, relativeFilePath),
@@ -46785,9 +47122,12 @@ function convertFromMusecodeFormat(musecodeMcp) {
46785
47122
  /**
46786
47123
  * Meta Muse Code MCP servers.
46787
47124
  *
46788
- * Muse Code reads MCP servers only from the `mcp_servers` (or `mcpServers`)
46789
- * block of the GLOBAL user settings file `~/.config/muse/settings.json`; no
46790
- * 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
46791
47131
  * `"schema_version": 1` — a file that omits that key fails every command at
46792
47132
  * startup with `malformed settings file` — so the key is bootstrapped when the
46793
47133
  * file is created and preserved when it already exists. Other settings keys are
@@ -46795,6 +47135,7 @@ function convertFromMusecodeFormat(musecodeMcp) {
46795
47135
  *
46796
47136
  * @see https://dev.meta.ai/docs/muse-code/configuration.md
46797
47137
  * @see https://dev.meta.ai/docs/muse-code/extending.md
47138
+ * @see https://dev.meta.ai/docs/muse-code/changelog
46798
47139
  */
46799
47140
  var MusecodeMcp = class MusecodeMcp extends ToolMcp {
46800
47141
  json;
@@ -48027,7 +48368,7 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
48027
48368
  feature: "mcp",
48028
48369
  existingContent,
48029
48370
  patch: {
48030
- mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers()),
48371
+ mcpServers: undefinedWhenEmpty(convertToQwencodeFormat(rulesyncMcp.getMcpServers())),
48031
48372
  ...mcpPatch !== void 0 && { mcp: mcpPatch }
48032
48373
  },
48033
48374
  filePath
@@ -55439,6 +55780,7 @@ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
55439
55780
  "dialogExpiry",
55440
55781
  "feedbackDrafts",
55441
55782
  "footerLinksRegexes",
55783
+ "forceLoginGatewayUrl",
55442
55784
  "modelPicker",
55443
55785
  "pluginConfigs",
55444
55786
  "prependPlugins",
@@ -55452,6 +55794,39 @@ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
55452
55794
  "vimInsertModeRemaps"
55453
55795
  ]);
55454
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
+ /**
55455
55830
  * Top-level settings keys neither file rulesync writes can honor, with the file
55456
55831
  * that does. `Managed` keys are read only from the settings file an organization
55457
55832
  * deploys, and `Global config` keys only from `~/.claude.json` — rulesync writes
@@ -55489,7 +55864,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
55489
55864
  disableMobileSimulatorTools: "managed settings",
55490
55865
  disableSideloadFlags: "managed settings",
55491
55866
  externalEditorContext: "~/.claude.json",
55492
- forceLoginGatewayUrl: "managed settings",
55493
55867
  forceRemoteSettingsRefresh: "managed settings",
55494
55868
  gatewayInternalNetworks: "managed settings",
55495
55869
  leftArrowOpensAgents: "~/.claude.json",
@@ -55648,8 +56022,9 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
55648
56022
  /**
55649
56023
  * `env` variables a project or local settings file cannot set, because a
55650
56024
  * checked-out repository should not control them: where Claude Code stores its
55651
- * own files, what session content it exports, where telemetry goes, and how it
55652
- * 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`.
55653
56028
  *
55654
56029
  * Only the names the settings reference lists are covered; its directory and
55655
56030
  * Windows groups are introduced with "such as", so an unlisted member of those
@@ -55660,6 +56035,10 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
55660
56035
  */
55661
56036
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
55662
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",
55663
56042
  "CLAUDE_CODE_ENABLE_TELEMETRY",
55664
56043
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
55665
56044
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
@@ -55668,6 +56047,7 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
55668
56047
  "CLAUDE_CODE_SYNC_PLUGINS",
55669
56048
  "CLAUDE_CODE_SYNC_SKILLS",
55670
56049
  "CLAUDE_CODE_TMPDIR",
56050
+ "CLAUDE_CODE_USER_DIALOG_TIMEOUT_MS",
55671
56051
  "CLAUDE_CONFIG_DIR",
55672
56052
  "ENABLE_BETA_TRACING_DETAILED",
55673
56053
  "ENABLE_ENHANCED_TELEMETRY_BETA",
@@ -55713,6 +56093,13 @@ const CLAUDECODE_ENV_CONTENT_OFF_VALUES = /* @__PURE__ */ new Set([
55713
56093
  * writing a value Claude Code ignores is the old behavior, while dropping an
55714
56094
  * opt-out it honors would remove a privacy setting. `OTEL_LOG_ASSISTANT_RESPONSES`
55715
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
55716
56103
  */
55717
56104
  const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
55718
56105
  OTEL_LOGS_EXPORTER: CLAUDECODE_ENV_EXPORTER_OFF_VALUES,
@@ -55720,7 +56107,8 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
55720
56107
  OTEL_TRACES_EXPORTER: CLAUDECODE_ENV_EXPORTER_OFF_VALUES,
55721
56108
  OTEL_LOG_USER_PROMPTS: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
55722
56109
  OTEL_LOG_TOOL_CONTENT: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
55723
- 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
55724
56112
  };
55725
56113
  function isProjectIgnoredEnvVariable(name, value) {
55726
56114
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_")) return true;
@@ -55757,7 +56145,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
55757
56145
  kept[name] = value;
55758
56146
  }
55759
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.`);
55760
- 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.`);
55761
56149
  return kept;
55762
56150
  }
55763
56151
  /**
@@ -55988,6 +56376,12 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
55988
56376
  relativeFilePath: paths.relativeFilePath,
55989
56377
  logger
55990
56378
  });
56379
+ stripUserScopeOnlyNestedPaths({
56380
+ overrides: overrideTopLevel,
56381
+ global,
56382
+ relativeFilePath: paths.relativeFilePath,
56383
+ logger
56384
+ });
55991
56385
  const { filtered: scopedTopLevel, trustAffecting: trustAffectingTopLevel } = stripUnhonoredTopLevelKeys({
55992
56386
  overrides: overrideTopLevel,
55993
56387
  global,
@@ -56405,7 +56799,7 @@ const CODEBUDDY_RULE_LISTS = [
56405
56799
  "ask",
56406
56800
  "deny"
56407
56801
  ];
56408
- const CATCH_ALL_PATTERN$11 = "*";
56802
+ const CATCH_ALL_PATTERN$12 = "*";
56409
56803
  const MCP_CATEGORY = "mcp";
56410
56804
  const MCP_RULE_PREFIX = "mcp__";
56411
56805
  const ALL_MCP_RULE = "mcp__*";
@@ -56478,7 +56872,7 @@ function categoryOfRule$2(rule) {
56478
56872
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
56479
56873
  */
56480
56874
  function buildCodebuddyRule({ category, pattern, action, logger }) {
56481
- const catchAll = pattern === CATCH_ALL_PATTERN$11;
56875
+ const catchAll = pattern === CATCH_ALL_PATTERN$12;
56482
56876
  if (category === MCP_CATEGORY) {
56483
56877
  if (catchAll && action === "allow") {
56484
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').`);
@@ -56487,7 +56881,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
56487
56881
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
56488
56882
  }
56489
56883
  if (category === "*") {
56490
- if (catchAll) return CATCH_ALL_PATTERN$11;
56884
+ if (catchAll) return CATCH_ALL_PATTERN$12;
56491
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').`);
56492
56886
  return null;
56493
56887
  }
@@ -56505,7 +56899,7 @@ function parseCodebuddyRule(rule) {
56505
56899
  const { specifier } = splitCodebuddyRule(rule);
56506
56900
  if (specifier === void 0) return {
56507
56901
  category,
56508
- pattern: CATCH_ALL_PATTERN$11
56902
+ pattern: CATCH_ALL_PATTERN$12
56509
56903
  };
56510
56904
  const trimmed = specifier.trim();
56511
56905
  return trimmed === "" ? null : {
@@ -58756,7 +59150,7 @@ function mapBashActionToDecision(action) {
58756
59150
  //#region src/features/permissions/commandcode-permissions.ts
58757
59151
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
58758
59152
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
58759
- const CATCH_ALL_PATTERN$10 = "*";
59153
+ const CATCH_ALL_PATTERN$11 = "*";
58760
59154
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
58761
59155
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
58762
59156
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -58781,14 +59175,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
58781
59175
  web_fetch: "webfetch",
58782
59176
  web_search: "websearch"
58783
59177
  };
58784
- const ACTION_RANK$4 = {
59178
+ const ACTION_RANK$5 = {
58785
59179
  allow: 0,
58786
59180
  ask: 1,
58787
59181
  deny: 2
58788
59182
  };
58789
59183
  /** Whether `action` outranks `existing` (deny > ask > allow). */
58790
59184
  function isStricterAction({ action, existing }) {
58791
- 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];
58792
59186
  }
58793
59187
  /**
58794
59188
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -58811,10 +59205,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
58811
59205
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
58812
59206
  */
58813
59207
  function buildCommandcodeRule(category, pattern) {
58814
- const catchAll = pattern === CATCH_ALL_PATTERN$10 || pattern === "";
59208
+ const catchAll = pattern === CATCH_ALL_PATTERN$11 || pattern === "";
58815
59209
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
58816
59210
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
58817
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$10 : null;
59211
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$11 : null;
58818
59212
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
58819
59213
  if (tool === void 0) return null;
58820
59214
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -58850,7 +59244,7 @@ function splitCommandcodeRule(rule) {
58850
59244
  }
58851
59245
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
58852
59246
  function isCatchAllSpecifier(inner) {
58853
- return inner === "" || inner === CATCH_ALL_PATTERN$10;
59247
+ return inner === "" || inner === CATCH_ALL_PATTERN$11;
58854
59248
  }
58855
59249
  /**
58856
59250
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -58884,34 +59278,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
58884
59278
  if (server.length === 0) return null;
58885
59279
  const everyServer = {
58886
59280
  category: "mcp",
58887
- pattern: CATCH_ALL_PATTERN$10,
59281
+ pattern: CATCH_ALL_PATTERN$11,
58888
59282
  notInAllow: true
58889
59283
  };
58890
59284
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
58891
- if (server === CATCH_ALL_PATTERN$10) return everyServer;
58892
- 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;
58893
59287
  return {
58894
- category: rest === "" || rest === CATCH_ALL_PATTERN$10 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
58895
- 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,
58896
59290
  notInAllow: false
58897
59291
  };
58898
59292
  }
58899
59293
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
58900
- 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);
58901
59295
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
58902
59296
  category,
58903
59297
  pattern: inner,
58904
59298
  notInAllow: true
58905
59299
  } : null;
58906
- if (remainder === CATCH_ALL_PATTERN$10) return everyServer;
58907
- 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 {
58908
59302
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
58909
- pattern: CATCH_ALL_PATTERN$10,
59303
+ pattern: CATCH_ALL_PATTERN$11,
58910
59304
  notInAllow: true
58911
59305
  };
58912
59306
  return namesTool ? {
58913
59307
  category,
58914
- pattern: CATCH_ALL_PATTERN$10,
59308
+ pattern: CATCH_ALL_PATTERN$11,
58915
59309
  notInAllow: false
58916
59310
  } : null;
58917
59311
  }
@@ -58935,14 +59329,14 @@ function friendlyToolRule({ category, inner }) {
58935
59329
  if (category === "bash") {
58936
59330
  if (isCatchAllSpecifier(inner)) return {
58937
59331
  category,
58938
- pattern: CATCH_ALL_PATTERN$10,
59332
+ pattern: CATCH_ALL_PATTERN$11,
58939
59333
  notInAllow: false
58940
59334
  };
58941
59335
  const normalized = inner.trim().replace(/\s+/g, " ");
58942
59336
  if (normalized === "" || normalized === ":*") return null;
58943
59337
  return isCatchAllSpecifier(normalized) ? {
58944
59338
  category,
58945
- pattern: CATCH_ALL_PATTERN$10,
59339
+ pattern: CATCH_ALL_PATTERN$11,
58946
59340
  notInAllow: true
58947
59341
  } : {
58948
59342
  category,
@@ -58952,7 +59346,7 @@ function friendlyToolRule({ category, inner }) {
58952
59346
  }
58953
59347
  if (isCatchAllSpecifier(inner)) return {
58954
59348
  category,
58955
- pattern: CATCH_ALL_PATTERN$10,
59349
+ pattern: CATCH_ALL_PATTERN$11,
58956
59350
  notInAllow: false
58957
59351
  };
58958
59352
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -58971,9 +59365,9 @@ function friendlyToolRule({ category, inner }) {
58971
59365
  */
58972
59366
  function parseCommandcodeRule(rule) {
58973
59367
  const { tool, inner } = splitCommandcodeRule(rule);
58974
- if (tool === CATCH_ALL_PATTERN$10) return isCatchAllSpecifier(inner) ? {
59368
+ if (tool === CATCH_ALL_PATTERN$11) return isCatchAllSpecifier(inner) ? {
58975
59369
  category: "*",
58976
- pattern: CATCH_ALL_PATTERN$10,
59370
+ pattern: CATCH_ALL_PATTERN$11,
58977
59371
  notInAllow: true
58978
59372
  } : null;
58979
59373
  const lowered = tool.toLowerCase();
@@ -59043,7 +59437,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
59043
59437
  }).filter((rule) => {
59044
59438
  const parsed = parseCommandcodeRule(rule);
59045
59439
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
59046
- 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})`);
59047
59441
  if (key !== "allow" && isStricterAction({
59048
59442
  action: key,
59049
59443
  existing: replacement
@@ -59100,7 +59494,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
59100
59494
  const ranked = /* @__PURE__ */ new Map();
59101
59495
  const managedCategories = /* @__PURE__ */ new Set();
59102
59496
  for (const [category, rules] of Object.entries(permission)) {
59103
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$10);
59497
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$11);
59104
59498
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
59105
59499
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
59106
59500
  for (const [pattern, action] of Object.entries(rules)) {
@@ -59371,7 +59765,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
59371
59765
  //#endregion
59372
59766
  //#region src/features/permissions/continue-permissions.ts
59373
59767
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
59374
- const CATCH_ALL_PATTERN$9 = "*";
59768
+ const CATCH_ALL_PATTERN$10 = "*";
59375
59769
  /**
59376
59770
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
59377
59771
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -59379,7 +59773,7 @@ const CATCH_ALL_PATTERN$9 = "*";
59379
59773
  * the bare entry instead of being skipped as a pattern-specific rule.
59380
59774
  */
59381
59775
  function normalizeCatchAllPattern$1(pattern) {
59382
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
59776
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
59383
59777
  }
59384
59778
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
59385
59779
  bash: "Bash",
@@ -59416,7 +59810,7 @@ function toCanonicalToolName$6(continueName) {
59416
59810
  * `Tool(pattern)`.
59417
59811
  */
59418
59812
  function buildContinuePermissionEntry(toolName, pattern) {
59419
- return pattern === CATCH_ALL_PATTERN$9 ? toolName : `${toolName}(${pattern})`;
59813
+ return pattern === CATCH_ALL_PATTERN$10 ? toolName : `${toolName}(${pattern})`;
59420
59814
  }
59421
59815
  /**
59422
59816
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -59430,7 +59824,7 @@ function parseContinuePermissionEntry(entry) {
59430
59824
  if (toolName === "") return;
59431
59825
  return {
59432
59826
  toolName,
59433
- pattern: pattern === "" ? CATCH_ALL_PATTERN$9 : pattern
59827
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$10 : pattern
59434
59828
  };
59435
59829
  }
59436
59830
  /**
@@ -59604,7 +59998,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
59604
59998
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
59605
59999
  continue;
59606
60000
  }
59607
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$9) {
60001
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$10) {
59608
60002
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
59609
60003
  continue;
59610
60004
  }
@@ -59972,14 +60366,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
59972
60366
  };
59973
60367
  //#endregion
59974
60368
  //#region src/features/permissions/crush-permissions.ts
59975
- const CATCH_ALL_PATTERN$8 = "*";
60369
+ const CATCH_ALL_PATTERN$9 = "*";
59976
60370
  /**
59977
60371
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
59978
60372
  * argument patterns at all, so it is the only pattern a rule can carry and
59979
60373
  * still be written.
59980
60374
  */
59981
60375
  function normalizeCatchAllPattern(pattern) {
59982
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
60376
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
59983
60377
  }
59984
60378
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
59985
60379
  bash: "bash",
@@ -60178,7 +60572,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
60178
60572
  for (const [rawPattern, action] of Object.entries(rules)) {
60179
60573
  if (isPrototypePollutionKey(rawPattern)) continue;
60180
60574
  const pattern = normalizeCatchAllPattern(rawPattern);
60181
- if (pattern === CATCH_ALL_PATTERN$8) {
60575
+ if (pattern === CATCH_ALL_PATTERN$9) {
60182
60576
  catchAllAction = action;
60183
60577
  continue;
60184
60578
  }
@@ -60251,11 +60645,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
60251
60645
  };
60252
60646
  for (const entry of disabled) {
60253
60647
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
60254
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] = "deny";
60648
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] = "deny";
60255
60649
  }
60256
60650
  for (const entry of allowed) {
60257
60651
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
60258
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] ??= "allow";
60652
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] ??= "allow";
60259
60653
  }
60260
60654
  return { permission };
60261
60655
  }
@@ -62120,7 +62514,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
62120
62514
  //#region src/features/permissions/goose-permissions.ts
62121
62515
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
62122
62516
  const GOOSE_USER_KEY = "user";
62123
- const CATCH_ALL_PATTERN$7 = "*";
62517
+ const CATCH_ALL_PATTERN$8 = "*";
62124
62518
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
62125
62519
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
62126
62520
  shell: ["bash"],
@@ -62132,7 +62526,7 @@ const ACTION_TO_GOOSE_LIST = {
62132
62526
  ask: "ask_before",
62133
62527
  deny: "never_allow"
62134
62528
  };
62135
- const ACTION_RANK$3 = {
62529
+ const ACTION_RANK$4 = {
62136
62530
  allow: 0,
62137
62531
  ask: 1,
62138
62532
  deny: 2
@@ -62284,14 +62678,14 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
62284
62678
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
62285
62679
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
62286
62680
  for (const [pattern, action] of Object.entries(rules)) {
62287
- if (pattern !== CATCH_ALL_PATTERN$7) {
62681
+ if (pattern !== CATCH_ALL_PATTERN$8) {
62288
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.`);
62289
62683
  continue;
62290
62684
  }
62291
62685
  const previous = assigned.get(toolName);
62292
62686
  if (previous === action) continue;
62293
62687
  if (previous !== void 0) {
62294
- 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;
62295
62689
  logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
62296
62690
  if (stricter === previous) continue;
62297
62691
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
@@ -62315,7 +62709,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
62315
62709
  const action = GOOSE_LIST_TO_ACTION[key];
62316
62710
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
62317
62711
  permission[category] ??= {};
62318
- permission[category][CATCH_ALL_PATTERN$7] = action;
62712
+ permission[category][CATCH_ALL_PATTERN$8] = action;
62319
62713
  }
62320
62714
  }
62321
62715
  return { permission };
@@ -62326,7 +62720,7 @@ const GROKCLI_UI_KEY = "ui";
62326
62720
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
62327
62721
  const GROKCLI_PERMISSION_KEY = "permission";
62328
62722
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
62329
- const CATCH_ALL_PATTERN$6 = "*";
62723
+ const CATCH_ALL_PATTERN$7 = "*";
62330
62724
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
62331
62725
  const CATEGORY_TO_GROK_TOOL = {
62332
62726
  bash: "Bash",
@@ -62351,6 +62745,8 @@ const GROK_TOOL_TO_CATEGORY = {
62351
62745
  ...Object.fromEntries(["SendSubagentMessage", "SendAgentMessage"].map((tool) => [tool, "agent"]))
62352
62746
  };
62353
62747
  const GROK_MCP_TOOL = "MCPTool";
62748
+ const GROK_ALL_TOOLS_ENTRY = "*";
62749
+ const GROK_VERBOSE_ANY_TOOL = "any";
62354
62750
  const GROK_TOOL_TO_CATEGORY_LOWER = {
62355
62751
  ...Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category])),
62356
62752
  agent_message: "agent"
@@ -62365,22 +62761,26 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
62365
62761
  * Scoped MCP categories (`mcp__<remainder>`) fold their address into the
62366
62762
  * parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
62367
62763
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
62368
- * 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`.
62369
62767
  */
62370
62768
  function buildGrokEntry(category, pattern) {
62769
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$7 ? GROK_ALL_TOOLS_ENTRY : null;
62371
62770
  if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
62372
62771
  const remainder = category.slice(5);
62373
62772
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
62374
62773
  }
62375
- 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})`;
62376
62775
  const tool = CATEGORY_TO_GROK_TOOL[category];
62377
62776
  if (tool === void 0) return null;
62378
- return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? tool : `${tool}(${pattern})`;
62777
+ return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? tool : `${tool}(${pattern})`;
62379
62778
  }
62380
62779
  /**
62381
62780
  * Parse a Grok Claude-style permission entry back into a canonical category +
62382
- * pattern. Entries without parentheses are treated as catch-all (`*`). Returns
62383
- * `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.
62384
62784
  */
62385
62785
  function parseGrokEntry(entry) {
62386
62786
  const trimmed = entry.trim();
@@ -62394,19 +62794,23 @@ function parseGrokEntry(entry) {
62394
62794
  tool = trimmed.slice(0, parenIndex);
62395
62795
  inner = trimmed.slice(parenIndex + 1, -1).trim();
62396
62796
  }
62797
+ if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
62798
+ category: "*",
62799
+ pattern: CATCH_ALL_PATTERN$7
62800
+ };
62397
62801
  if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$3)) return parseClaudeStyleMcpEntry(tool);
62398
62802
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
62399
62803
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
62400
- pattern: CATCH_ALL_PATTERN$6
62804
+ pattern: CATCH_ALL_PATTERN$7
62401
62805
  } : {
62402
62806
  category: "mcp",
62403
- pattern: CATCH_ALL_PATTERN$6
62807
+ pattern: CATCH_ALL_PATTERN$7
62404
62808
  };
62405
62809
  const category = GROK_TOOL_TO_CATEGORY[tool];
62406
62810
  if (category === void 0) return null;
62407
62811
  return {
62408
62812
  category,
62409
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$6
62813
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$7
62410
62814
  };
62411
62815
  }
62412
62816
  /**
@@ -62423,14 +62827,14 @@ function parseGrokEntry(entry) {
62423
62827
  function parseClaudeStyleMcpEntry(entry) {
62424
62828
  const rest = entry.slice(5);
62425
62829
  if (rest.length === 0) return null;
62426
- if (rest === CATCH_ALL_PATTERN$6) return {
62830
+ if (rest === CATCH_ALL_PATTERN$7) return {
62427
62831
  category: "mcp",
62428
- pattern: CATCH_ALL_PATTERN$6
62832
+ pattern: CATCH_ALL_PATTERN$7
62429
62833
  };
62430
62834
  const address = rest.includes("__") ? rest : `${rest}__*`;
62431
62835
  return {
62432
62836
  category: `${MCP_CANONICAL_PREFIX$3}${address}`,
62433
- pattern: CATCH_ALL_PATTERN$6
62837
+ pattern: CATCH_ALL_PATTERN$7
62434
62838
  };
62435
62839
  }
62436
62840
  /**
@@ -62451,15 +62855,20 @@ function parseGrokRule(rule) {
62451
62855
  if (typeof tool !== "string") return null;
62452
62856
  if (pattern !== void 0 && typeof pattern !== "string") return null;
62453
62857
  const trimmedPattern = pattern?.trim() ?? "";
62454
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$6;
62858
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$7;
62455
62859
  const lowerTool = tool.trim().toLowerCase();
62456
- 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 ? {
62457
62866
  category: "mcp",
62458
- pattern: CATCH_ALL_PATTERN$6,
62867
+ pattern: CATCH_ALL_PATTERN$7,
62459
62868
  action
62460
62869
  } : {
62461
62870
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
62462
- pattern: CATCH_ALL_PATTERN$6,
62871
+ pattern: CATCH_ALL_PATTERN$7,
62463
62872
  action
62464
62873
  };
62465
62874
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -62489,7 +62898,10 @@ function parseGrokRule(rule) {
62489
62898
  * subagent rather than its launch, and whose pattern is a subagent id
62490
62899
  * rather than a subagent type); `mcp__*` maps to `MCPTool(...)` (a scoped
62491
62900
  * MCP category folds its address into the parentheses, so a non-`*`
62492
- * 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
62493
62905
  * skipped (with a warning when it carries a `deny` rule, to surface the
62494
62906
  * gap). When two canonical rules collapse onto the same Grok
62495
62907
  * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
@@ -62500,7 +62912,9 @@ function parseGrokRule(rule) {
62500
62912
  * verbose `[[permission.rules]]` tables
62501
62913
  * (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
62502
62914
  * field is matched case-insensitively against the same tool table, plus
62503
- * 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
62504
62918
  * from the two forms are merged with the same `deny > ask > allow`
62505
62919
  * precedence. When neither form carries a rule (older configs), the coarse
62506
62920
  * `[ui] permission_mode` is used as a fallback. Generate always emits the
@@ -62525,7 +62939,7 @@ function parseGrokRule(rule) {
62525
62939
  * tools it models and (in global scope) the `[ui] permission_mode` value, while
62526
62940
  * every other key
62527
62941
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
62528
- * 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.
62529
62943
  * The file is never deleted.
62530
62944
  */
62531
62945
  var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
@@ -62607,7 +63021,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
62607
63021
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
62608
63022
  }
62609
63023
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
62610
- 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) } } };
62611
63025
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
62612
63026
  }
62613
63027
  validate() {
@@ -62627,14 +63041,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
62627
63041
  });
62628
63042
  }
62629
63043
  };
62630
- const ACTION_RANK$2 = {
63044
+ const ACTION_RANK$3 = {
62631
63045
  allow: 0,
62632
63046
  ask: 1,
62633
63047
  deny: 2
62634
63048
  };
62635
63049
  /**
62636
63050
  * Collect user-authored entries from an existing `[permission]` array whose
62637
- * 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
62638
63052
  * preserved verbatim so replacing the arrays does not silently drop them
62639
63053
  * (mirrors the Cursor adapter's preservation of unmanaged types).
62640
63054
  */
@@ -62653,13 +63067,26 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
62653
63067
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
62654
63068
  const entry = buildGrokEntry(category, pattern);
62655
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
+ }
62656
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.`);
62657
63075
  continue;
62658
63076
  }
62659
63077
  const existing = ranked.get(entry);
62660
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).`);
62661
- 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);
62662
63080
  }
63081
+ warnRemovedAllToolsRestrictions({
63082
+ existingPermission,
63083
+ ranked,
63084
+ logger
63085
+ });
63086
+ warnEntriesShadowedByAllTools({
63087
+ ranked,
63088
+ logger
63089
+ });
62663
63090
  const allow = unmanagedEntries(existingPermission, "allow");
62664
63091
  const deny = unmanagedEntries(existingPermission, "deny");
62665
63092
  const ask = unmanagedEntries(existingPermission, "ask");
@@ -62673,6 +63100,38 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
62673
63100
  };
62674
63101
  }
62675
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
+ /**
62676
63135
  * Parse Grok's `[permission]` section back into a canonical permission map,
62677
63136
  * reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
62678
63137
  * Claude-style entries and the verbose `[[permission.rules]]` tables
@@ -62691,7 +63150,7 @@ function parseGrokPermissionArrays(permission) {
62691
63150
  const record = ({ category, pattern, action }) => {
62692
63151
  const bucket = result[category] ??= {};
62693
63152
  const existing = bucket[pattern];
62694
- 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;
62695
63154
  };
62696
63155
  const applyEntries = (entries, action) => {
62697
63156
  for (const entry of entries ?? []) {
@@ -63095,7 +63554,7 @@ const JUNIE_GROUP_TO_CANONICAL = {
63095
63554
  mcpTools: "mcp",
63096
63555
  readOutsideProject: "read"
63097
63556
  };
63098
- function isPermissionAction$1(value) {
63557
+ function isPermissionAction$2(value) {
63099
63558
  return PermissionActionSchema.safeParse(value).success;
63100
63559
  }
63101
63560
  /**
@@ -63281,7 +63740,7 @@ function convertJunieToRulesyncPermissions({ allowlist }) {
63281
63740
  for (const rule of Array.isArray(ruleSet.rules) ? ruleSet.rules : []) {
63282
63741
  if (!rule || typeof rule !== "object") continue;
63283
63742
  const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
63284
- if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
63743
+ if (pattern === void 0 || !isPermissionAction$2(rule.action)) continue;
63285
63744
  (permission[category] ??= {})[pattern] = rule.action;
63286
63745
  }
63287
63746
  }
@@ -64240,6 +64699,367 @@ function rulesFromArrays(settings, allowKey, denyKey) {
64240
64699
  return rules;
64241
64700
  }
64242
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
64243
65063
  //#region src/features/permissions/lettacode-permissions.ts
64244
65064
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
64245
65065
  const LETTACODE_PERMISSIONS_KEY = "permissions";
@@ -66267,6 +67087,8 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
66267
67087
  "eager",
66268
67088
  "listDirectory",
66269
67089
  "todoWrite",
67090
+ "codeModeOnly",
67091
+ "freeform",
66270
67092
  "workflowsEnabled",
66271
67093
  "workflowNameOnly",
66272
67094
  "executionSandbox"
@@ -66393,6 +67215,16 @@ const QWEN_SCOPED_TOOLS_KEYS = {
66393
67215
  rule: "global-machine-wide",
66394
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.`,
66395
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."
66396
67228
  }
66397
67229
  };
66398
67230
  /**
@@ -66856,22 +67688,42 @@ function convertQwenToRulesyncPermissions(params) {
66856
67688
  //#endregion
66857
67689
  //#region src/features/permissions/reasonix-permissions.ts
66858
67690
  /**
66859
- * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
66860
- * permission-rule tool families (PascalCase).
66861
- *
66862
- * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
66863
- * documented as "Claude Code-style": "Bash and file mutation approvals use
66864
- * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
66865
- * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
66866
- * IDs for compatibility, but new rules are saved using these PascalCase
66867
- * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
66868
- * uses (the closest documented precedent for this syntax).
66869
- * @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
66870
67705
  */
66871
67706
  const CANONICAL_TO_REASONIX_TOOL_NAMES = {
66872
67707
  bash: "Bash",
66873
- read: "Read",
67708
+ read: REASONIX_READ_TOOL_NAME,
66874
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,
66875
67727
  write: "Write",
66876
67728
  webfetch: "WebFetch",
66877
67729
  websearch: "WebSearch",
@@ -66881,9 +67733,16 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
66881
67733
  agent: "Agent"
66882
67734
  };
66883
67735
  /**
66884
- * 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.
66885
67738
  */
66886
- 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
+ }
66887
67746
  function toReasonixToolName(canonical) {
66888
67747
  return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
66889
67748
  }
@@ -67156,7 +68015,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
67156
68015
  const parsed = parseReasonixConfig(existingContent);
67157
68016
  const config = rulesyncPermissions.getJson();
67158
68017
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
67159
- 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)));
67160
68019
  const override = config.reasonix;
67161
68020
  const rawAllow = toReasonixStringArray(override?.rawAllow);
67162
68021
  const rawAsk = toReasonixStringArray(override?.rawAsk);
@@ -67173,12 +68032,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
67173
68032
  rawOwnedEntries,
67174
68033
  logger
67175
68034
  });
67176
- if (logger && managedToolNames.has("Read")) {
68035
+ if (logger && managedToolNames.has("read_file")) {
68036
+ const readToolNames = new Set(reasonixToolNamesOf("read"));
67177
68037
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
67178
68038
  const { toolName } = parseReasonixPermissionEntry(entry);
67179
- return toolName === "Read";
68039
+ return readToolNames.has(toolName);
67180
68040
  });
67181
- 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.`);
67182
68042
  }
67183
68043
  const mergedPermissions = { ...existingPermissions };
67184
68044
  setOrDeleteEntries(mergedPermissions, "allow", [
@@ -72524,18 +73384,18 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
72524
73384
  }
72525
73385
  }],
72526
73386
  ["kiro-cli", {
72527
- class: KiroPermissions,
73387
+ class: KiroScopedPermissions,
72528
73388
  meta: {
72529
73389
  supportsProject: true,
72530
- supportsGlobal: false,
73390
+ supportsGlobal: true,
72531
73391
  supportsImport: true
72532
73392
  }
72533
73393
  }],
72534
73394
  ["kiro-ide", {
72535
- class: KiroPermissions,
73395
+ class: KiroScopedPermissions,
72536
73396
  meta: {
72537
73397
  supportsProject: true,
72538
- supportsGlobal: false,
73398
+ supportsGlobal: true,
72539
73399
  supportsImport: true
72540
73400
  }
72541
73401
  }],
@@ -78522,6 +79382,24 @@ var KimiCodeSkill = class extends ToolSkill {
78522
79382
  const targets = rulesyncSkill.getFrontmatter().targets;
78523
79383
  return targets.includes("*") || targets.includes("kimi-code");
78524
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
+ }
78525
79403
  static async fromDir(params) {
78526
79404
  const loaded = await this.loadSkillDirContent({
78527
79405
  ...params,
@@ -78595,6 +79473,14 @@ var KimiCodePluginSkill = class extends KimiCodeSkill {
78595
79473
  return { relativeDirPath: KIMI_CODE_PLUGIN_SKILLS_DIR };
78596
79474
  }
78597
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
+ /**
78598
79484
  * The bundle is read from `--output-root <plugin>`, but the imported rulesync
78599
79485
  * file belongs to the project's `.rulesync/`, not to the plugin root.
78600
79486
  */
@@ -78946,7 +79832,8 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
78946
79832
  license: zod_mini.z.optional(zod_mini.z.unknown()),
78947
79833
  compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
78948
79834
  metadata: zod_mini.z.optional(zod_mini.z.unknown()),
78949
- "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())
78950
79837
  });
78951
79838
  var OpenCodeSkill = class extends ToolSkill {
78952
79839
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
@@ -79063,7 +79950,8 @@ var OpenCodeSkill = class extends ToolSkill {
79063
79950
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
79064
79951
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
79065
79952
  ...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
79066
- ...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"] }
79067
79955
  };
79068
79956
  const { toolTarget } = this.constructor.layout;
79069
79957
  const rulesyncFrontmatter = {
@@ -79098,13 +79986,18 @@ var OpenCodeSkill = class extends ToolSkill {
79098
79986
  rootFrontmatter: rulesyncFrontmatter,
79099
79987
  section: opencodeSection
79100
79988
  });
79989
+ const disableModelInvocation = resolveDisableModelInvocation({
79990
+ rootFrontmatter: rulesyncFrontmatter,
79991
+ section: opencodeSection
79992
+ });
79101
79993
  const opencodeFrontmatter = {
79102
79994
  name: rulesyncFrontmatter.name,
79103
79995
  description: rulesyncFrontmatter.description,
79104
79996
  ...license !== void 0 && { license },
79105
79997
  ...compatibility !== void 0 && { compatibility },
79106
79998
  ...metadata !== void 0 && { metadata },
79107
- ...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 }
79108
80001
  };
79109
80002
  const settablePaths = this.getSettablePaths({ global });
79110
80003
  return new this({
@@ -79127,11 +80020,13 @@ var OpenCodeSkill = class extends ToolSkill {
79127
80020
  ...params,
79128
80021
  getSettablePaths: (options) => this.getSettablePaths(options)
79129
80022
  });
79130
- const result = OpenCodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
79131
- if (!result.success) {
79132
- const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
79133
- throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
79134
- }
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)}`);
79135
80030
  return new this({
79136
80031
  outputRoot: loaded.outputRoot,
79137
80032
  relativeDirPath: loaded.relativeDirPath,
@@ -79159,6 +80054,52 @@ var OpenCodeSkill = class extends ToolSkill {
79159
80054
  });
79160
80055
  }
79161
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
+ }
79162
80103
  //#endregion
79163
80104
  //#region src/features/skills/mimocode-skill.ts
79164
80105
  /**
@@ -100664,13 +101605,15 @@ const ANTIGRAVITY_PLUGIN_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/;
100664
101605
  * The manifest declares only `name` and `description`, and rejects anything
100665
101606
  * else, so the one required field is derived from the plugin directory's name.
100666
101607
  * An existing manifest is never read or rewritten: it may carry a
100667
- * 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.
100668
101611
  */
100669
101612
  async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger }) {
100670
101613
  const paths = [];
100671
101614
  for (const outputRoot of outputRoots) {
100672
101615
  const filePath = (0, node_path.join)(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME);
100673
- if (await fileExists(filePath)) continue;
101616
+ if (await hasAntigravityPluginManifest(outputRoot)) continue;
100674
101617
  if (await refusesWriteOutsideRoot({
100675
101618
  logger,
100676
101619
  rootPath: outputRoot,
@@ -100693,6 +101636,11 @@ async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger })
100693
101636
  sourceLoadFailed: false
100694
101637
  };
100695
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
+ }
100696
101644
  //#endregion
100697
101645
  //#region src/features/shared/hermes-project-plugin-activation.ts
100698
101646
  function mergeEnabledPlugins({ existingContent, filePath, pluginNames }) {
@@ -103752,6 +104700,12 @@ Object.defineProperty(exports, "loadYaml", {
103752
104700
  return loadYaml;
103753
104701
  }
103754
104702
  });
104703
+ Object.defineProperty(exports, "mapWithConcurrency", {
104704
+ enumerable: true,
104705
+ get: function() {
104706
+ return mapWithConcurrency;
104707
+ }
104708
+ });
103755
104709
  Object.defineProperty(exports, "mergeInputRootConfigs", {
103756
104710
  enumerable: true,
103757
104711
  get: function() {