rulesync 34.0.1 → 36.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -583,6 +583,7 @@ const ignoreProcessorToolTargetTuple = [
583
583
  "reasonix",
584
584
  "roo",
585
585
  "tabnine",
586
+ "trae",
586
587
  "devin",
587
588
  "vibe",
588
589
  "warp",
@@ -676,6 +677,7 @@ const commandsProcessorToolTargetTuple = [
676
677
  "hermesagent",
677
678
  "junie",
678
679
  "kilo",
680
+ "kimi-code",
679
681
  "kimi-code-plugin",
680
682
  "kiro",
681
683
  "kiro-cli",
@@ -692,6 +694,7 @@ const commandsProcessorToolTargetTuple = [
692
694
  "rovodev",
693
695
  "tabnine",
694
696
  "takt",
697
+ "trae",
695
698
  "devin",
696
699
  "warp",
697
700
  "zcode",
@@ -1383,20 +1386,22 @@ async function fileExists(filepath) {
1383
1386
  return false;
1384
1387
  }
1385
1388
  }
1386
- async function getFileSize(filepath) {
1389
+ /**
1390
+ * The size of a file and whether it carries any execute bit, read with a single
1391
+ * `stat`. Windows reports no execute bits, so a file there is never seen as
1392
+ * executable.
1393
+ */
1394
+ async function getFileSizeAndExecutableBit(filepath) {
1387
1395
  try {
1388
- return (await stat(filepath)).size;
1396
+ const stats = await stat(filepath);
1397
+ return {
1398
+ size: stats.size,
1399
+ executable: (stats.mode & 73) !== 0
1400
+ };
1389
1401
  } catch (error) {
1390
- throw new Error(`Failed to get file size for "${filepath}": ${formatError(error)}`, { cause: error });
1402
+ throw new Error(`Failed to stat "${filepath}": ${formatError(error)}`, { cause: error });
1391
1403
  }
1392
1404
  }
1393
- /**
1394
- * Whether a file carries any execute bit. Windows reports none, so a file
1395
- * there is never seen as executable.
1396
- */
1397
- async function hasExecutableBit(filepath) {
1398
- return ((await stat(filepath)).mode & 73) !== 0;
1399
- }
1400
1405
  async function isSymlink(filepath) {
1401
1406
  try {
1402
1407
  return (await lstat(filepath)).isSymbolicLink();
@@ -5267,7 +5272,7 @@ const JUNIE_HOOK_EVENTS = [
5267
5272
  * `PreToolUse`, and on the end reason for `SessionEnd`. Only `type: "command"`
5268
5273
  * hooks are supported.
5269
5274
  * @see https://docs.gitlab.com/user/gitlab_duo_cli/customize/
5270
- * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/core/hooks/src/schema.ts
5275
+ * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/v9.28.0/packages/core/hooks/src/schema.ts
5271
5276
  */
5272
5277
  const GITLABDUO_HOOK_EVENTS = [
5273
5278
  "sessionStart",
@@ -5621,6 +5626,7 @@ const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
5621
5626
  *
5622
5627
  * @see https://www.codebuddy.ai/docs/cli/hooks
5623
5628
  * @see https://www.codebuddy.ai/docs/cli/plugins-reference
5629
+ * @see https://www.npmjs.com/package/@tencent-ai/codebuddy-code/v/2.167.0
5624
5630
  */
5625
5631
  const CODEBUDDY_HOOK_EVENTS = [
5626
5632
  "sessionStart",
@@ -8146,7 +8152,8 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8146
8152
  * autonomy/sandbox controls with no canonical permission category — under
8147
8153
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
8148
8154
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
8149
- * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
8155
+ * `todoWrite`, `codeModeOnly`, `freeform`, `workflowsEnabled`, `workflowNameOnly`,
8156
+ * `executionSandbox`) and
8150
8157
  * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
8151
8158
  * `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
8152
8159
  * `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
@@ -8165,7 +8172,8 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8165
8172
  * changes what the file said is reported in either scope, naming what that key
8166
8173
  * decides there — the autonomy and containment
8167
8174
  * controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
8168
- * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
8175
+ * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`,
8176
+ * `codeModeOnly`, `freeform`, and `eager`, which
8169
8177
  * demotes an omitted tool to deferred rather than removing it, replacing rather
8170
8178
  * than merging with the list a higher scope set), the Auto Mode
8171
8179
  * classifier config, the skill kill-switches, and, because these groups are
@@ -10858,13 +10866,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
10858
10866
  "allowed-tools": z.optional(z.array(z.string())),
10859
10867
  license: z.optional(z.unknown()),
10860
10868
  compatibility: z.optional(z.unknown()),
10861
- metadata: z.optional(z.unknown())
10869
+ metadata: z.optional(z.unknown()),
10870
+ "disable-model-invocation": z.optional(z.boolean())
10862
10871
  })),
10863
10872
  mimocode: z.optional(z.looseObject({
10864
10873
  "allowed-tools": z.optional(z.array(z.string())),
10865
10874
  license: z.optional(z.unknown()),
10866
10875
  compatibility: z.optional(z.unknown()),
10867
- metadata: z.optional(z.unknown())
10876
+ metadata: z.optional(z.unknown()),
10877
+ "disable-model-invocation": z.optional(z.boolean())
10868
10878
  })),
10869
10879
  kilo: z.optional(z.looseObject({
10870
10880
  "allowed-tools": z.optional(z.array(z.string())),
@@ -11317,7 +11327,7 @@ async function getLocalSkillDirNames(sourceTree) {
11317
11327
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
11318
11328
  * default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11319
11329
  * copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
11320
- * commandcode, lettacode). Each tool's own section may override that
11330
+ * commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
11321
11331
  * default with a per-target value. A defined section value (including `false`)
11322
11332
  * always wins over the root default.
11323
11333
  *
@@ -13516,8 +13526,8 @@ function diffSharedConfigOwnedKeys({ fileKey, existingContent, newContent }) {
13516
13526
  return operations;
13517
13527
  }
13518
13528
  const READ_TOOL_NAME = "Read";
13519
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
13520
- const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
13529
+ const isReadDenyEntry = (entry, readToolName = READ_TOOL_NAME) => entry.startsWith(`${readToolName}(`) && entry.endsWith(")");
13530
+ const buildReadDenyEntry = (pattern, readToolName = READ_TOOL_NAME) => `${readToolName}(${pattern})`;
13521
13531
  const parsePermissionsBlock = (settings) => {
13522
13532
  const permissions = settings.permissions ?? {};
13523
13533
  return {
@@ -13541,9 +13551,9 @@ const withPermissions = (settings, next) => {
13541
13551
  };
13542
13552
  };
13543
13553
  const applyIgnoreReadDenies = (params) => {
13544
- const { settings, readDenies } = params;
13554
+ const { settings, readDenies, readToolName = READ_TOOL_NAME } = params;
13545
13555
  const current = parsePermissionsBlock(settings);
13546
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
13556
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry, readToolName) || readDenies.includes(entry));
13547
13557
  return withPermissions(settings, {
13548
13558
  allow: current.allow,
13549
13559
  ask: current.ask,
@@ -17870,6 +17880,7 @@ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
17870
17880
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
17871
17881
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
17872
17882
  const ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME = "plugin.json";
17883
+ const ANTIGRAVITY_PLUGIN_MANIFEST_FOLDERS = [".antigravity-plugin", ".gemini-plugin"];
17873
17884
  const AUGMENTCODE_PLUGIN_RULES_DIR = "rules";
17874
17885
  const AUGMENTCODE_PLUGIN_COMMANDS_DIR = "commands";
17875
17886
  const AUGMENTCODE_PLUGIN_AGENTS_DIR = "agents";
@@ -19516,9 +19527,36 @@ function commandSlug(relativeFilePath) {
19516
19527
  * skill. Once the command is removed from every source tree's `commands/`
19517
19528
  * directory, the directory stops matching and the skills feature cleans
19518
19529
  * it up as a regular orphan.
19530
+ *
19531
+ * When `toolTarget` is given, only commands whose `targets` include it (or
19532
+ * `*`) count: a command aimed at other tools never reaches this tool's skills
19533
+ * tree, so it must not shadow a same-named skill there. Commands that cannot
19534
+ * be parsed are skipped, as the commands feature reports them itself.
19519
19535
  */
19520
- async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
19521
- return (await Promise.all(inputRoots.map((root) => findFilesByGlobs("**/*.md", { cwd: join(root, COMMANDS_FEATURE_SUBDIR) })))).flat().some((filePath) => commandSlug(basename(filePath)) === dirName);
19536
+ async function rulesyncCommandSlugExists({ inputRoots, dirName, toolTarget }) {
19537
+ const matching = (await Promise.all(inputRoots.map(async (root) => {
19538
+ const commandsDirPath = join(root, COMMANDS_FEATURE_SUBDIR);
19539
+ return (await findFilesByGlobs("**/*.md", { cwd: commandsDirPath })).map((filePath) => ({
19540
+ root,
19541
+ commandsDirPath,
19542
+ filePath
19543
+ }));
19544
+ }))).flat().filter(({ filePath }) => commandSlug(basename(filePath)) === dirName);
19545
+ if (toolTarget === void 0) return matching.length > 0;
19546
+ for (const { root, commandsDirPath, filePath } of matching) {
19547
+ let targets;
19548
+ try {
19549
+ targets = (await RulesyncCommand.fromFile({
19550
+ outputRoot: root,
19551
+ relativeDirPath: COMMANDS_FEATURE_SUBDIR,
19552
+ relativeFilePath: relative(commandsDirPath, filePath)
19553
+ })).getFrontmatter().targets;
19554
+ } catch {
19555
+ continue;
19556
+ }
19557
+ if (targets.includes("*") || targets.includes(toolTarget)) return true;
19558
+ }
19559
+ return false;
19522
19560
  }
19523
19561
  //#endregion
19524
19562
  //#region src/features/commands/devin-command.ts
@@ -19536,7 +19574,7 @@ const DEVIN_COMMAND_SECTION_KEYS = [
19536
19574
  "permissions",
19537
19575
  "triggers"
19538
19576
  ];
19539
- function commandSkillContent$1(rulesyncCommand) {
19577
+ function commandSkillContent$2(rulesyncCommand) {
19540
19578
  const slug = commandSlug(rulesyncCommand.getRelativeFilePath());
19541
19579
  const frontmatter = rulesyncCommand.getFrontmatter();
19542
19580
  const description = frontmatter.description ?? `${slug} command`;
@@ -19610,7 +19648,7 @@ var DevinCommand = class DevinCommand extends ToolCommand {
19610
19648
  relativeDirPath: paths.relativeDirPath,
19611
19649
  relativeFilePath: SKILL_FILE_NAME,
19612
19650
  slug: commandSlug(rulesyncCommand.getRelativeFilePath()),
19613
- fileContent: commandSkillContent$1(rulesyncCommand)
19651
+ fileContent: commandSkillContent$2(rulesyncCommand)
19614
19652
  });
19615
19653
  }
19616
19654
  getFileContent() {
@@ -21430,6 +21468,180 @@ var KiloCommand = class KiloCommand extends ToolCommand {
21430
21468
  }
21431
21469
  };
21432
21470
  //#endregion
21471
+ //#region src/constants/kimi-code-paths.ts
21472
+ const KIMI_CODE_DIR = ".kimi-code";
21473
+ const KIMI_CODE_RULE_FILE_NAME = "AGENTS.md";
21474
+ const KIMI_CODE_MCP_FILE_NAME = "mcp.json";
21475
+ const KIMI_CODE_CONFIG_FILE_NAME = "config.toml";
21476
+ const KIMI_CODE_SKILLS_DIR_NAME = "skills";
21477
+ const KIMI_CODE_AGENTS_DIR_NAME = "agents";
21478
+ const KIMI_CODE_SKILLS_DIR_PATH = join(KIMI_CODE_DIR, KIMI_CODE_SKILLS_DIR_NAME);
21479
+ join(KIMI_CODE_DIR, KIMI_CODE_AGENTS_DIR_NAME);
21480
+ const KIMI_CODE_SHARED_SKILLS_DIR_PATH = join(".agents", "skills");
21481
+ const KIMI_CODE_SHARED_AGENTS_DIR_PATH = join(".agents", "agents");
21482
+ //#endregion
21483
+ //#region src/utils/kimi-code.ts
21484
+ function getKimiCodeHome() {
21485
+ const configuredHome = process.env.KIMI_CODE_HOME?.trim();
21486
+ return configuredHome ? resolve(configuredHome) : void 0;
21487
+ }
21488
+ function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
21489
+ return global && getKimiCodeHome() ? relativeDirPath : join(KIMI_CODE_DIR, relativeDirPath);
21490
+ }
21491
+ /**
21492
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
21493
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
21494
+ * Declared unconditionally so the derived shared-file keys — and the drift
21495
+ * guards checked against them — do not depend on the ambient environment.
21496
+ */
21497
+ function getKimiCodeSharedConfigWritePaths() {
21498
+ return [{
21499
+ relativeDirPath: KIMI_CODE_DIR,
21500
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21501
+ }, {
21502
+ relativeDirPath: ".",
21503
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21504
+ }];
21505
+ }
21506
+ /**
21507
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
21508
+ * Both spellings carry the same declaration, but passing the key of the file
21509
+ * being written keeps the write path and the drift guards on the same entry.
21510
+ */
21511
+ function getKimiCodeConfigSharedFileKey({ global }) {
21512
+ return sharedConfigFileKey({
21513
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
21514
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21515
+ });
21516
+ }
21517
+ function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
21518
+ return getToolRulesyncOutputRoot({
21519
+ nativeOutputRoot,
21520
+ global,
21521
+ toolHome: getKimiCodeHome
21522
+ });
21523
+ }
21524
+ //#endregion
21525
+ //#region src/features/commands/kimi-code-command.ts
21526
+ /**
21527
+ * Kimi Code SKILL.md frontmatter keys a command may author through its
21528
+ * `kimi-code:` section, each with the alias spellings Kimi also accepts. An
21529
+ * alias is normalized to the canonical key; the canonical spelling wins when
21530
+ * both are present.
21531
+ * @see https://moonshotai.github.io/kimi-code/en/customization/skills.html
21532
+ */
21533
+ const KIMI_CODE_COMMAND_SECTION_KEYS = [
21534
+ {
21535
+ key: "type",
21536
+ aliases: []
21537
+ },
21538
+ {
21539
+ key: "whenToUse",
21540
+ aliases: ["when-to-use", "when_to_use"]
21541
+ },
21542
+ {
21543
+ key: "disableModelInvocation",
21544
+ aliases: ["disable-model-invocation", "disable_model_invocation"]
21545
+ },
21546
+ {
21547
+ key: "arguments",
21548
+ aliases: []
21549
+ }
21550
+ ];
21551
+ function pickKimiCodeSectionKeys(section) {
21552
+ const picked = {};
21553
+ for (const { key, aliases } of KIMI_CODE_COMMAND_SECTION_KEYS) {
21554
+ const spelling = [key, ...aliases].find((candidate) => section[candidate] !== void 0);
21555
+ if (spelling !== void 0) picked[key] = section[spelling];
21556
+ }
21557
+ return picked;
21558
+ }
21559
+ function commandSkillContent$1(rulesyncCommand) {
21560
+ const slug = commandSlug(rulesyncCommand.getRelativeFilePath());
21561
+ const frontmatter = rulesyncCommand.getFrontmatter();
21562
+ const description = frontmatter.description?.trim() || `${slug} command`;
21563
+ return stringifyFrontmatter(rulesyncCommand.getBody().trim(), {
21564
+ name: slug,
21565
+ description,
21566
+ disableModelInvocation: true,
21567
+ ...pickKimiCodeSectionKeys(frontmatter["kimi-code"] ?? {})
21568
+ });
21569
+ }
21570
+ /**
21571
+ * Represents a Kimi Code slash command, emitted as a Kimi Code Skill.
21572
+ *
21573
+ * Kimi Code has no standalone custom-command directory outside plugins: every
21574
+ * external Skill is registered as a slash command (`/skill:<name>`, with a
21575
+ * `/<name>` shorthand when the bare name is free), and the Skill body supports
21576
+ * `$ARGUMENTS`, positional `$0` / `$1`, and named `$<name>` placeholders.
21577
+ * Commands are therefore emitted onto the native skills surface, one
21578
+ * `SKILL.md` per command: `.kimi-code/skills/<slug>/SKILL.md` (project) and
21579
+ * `~/.kimi-code/skills/<slug>/SKILL.md` (global; `$KIMI_CODE_HOME/skills/`
21580
+ * when `KIMI_CODE_HOME` is set). Directory-form `SKILL.md` requires both
21581
+ * `name` and `description`, so both are always written.
21582
+ *
21583
+ * Import and deletion are intentionally no-ops for this target: the skills
21584
+ * feature owns the `.kimi-code/skills/` tree, so importing it as commands
21585
+ * would double-import every skill (mirrors the Devin, Warp and Hermes Agent
21586
+ * commands targets).
21587
+ *
21588
+ * @see https://moonshotai.github.io/kimi-code/en/customization/skills.html
21589
+ * @see https://moonshotai.github.io/kimi-code/en/reference/slash-commands.html
21590
+ */
21591
+ var KimiCodeCommand = class KimiCodeCommand extends ToolCommand {
21592
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
21593
+ return this.isTargetedByRulesyncCommandDefault({
21594
+ rulesyncCommand,
21595
+ toolTarget: "kimi-code"
21596
+ });
21597
+ }
21598
+ static getSettablePaths({ global = false } = {}) {
21599
+ return { relativeDirPath: getKimiCodeRelativeDirPath({
21600
+ global,
21601
+ relativeDirPath: KIMI_CODE_SKILLS_DIR_NAME
21602
+ }) };
21603
+ }
21604
+ constructor({ slug, ...params }) {
21605
+ super({
21606
+ ...params,
21607
+ ...slug !== void 0 && {
21608
+ relativeDirPath: join(params.relativeDirPath, slug),
21609
+ relativeFilePath: "SKILL.md"
21610
+ }
21611
+ });
21612
+ }
21613
+ validate() {
21614
+ return {
21615
+ success: true,
21616
+ error: null
21617
+ };
21618
+ }
21619
+ toRulesyncCommand() {
21620
+ const slug = basename(dirname(this.getRelativePathFromCwd()));
21621
+ const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getFilePath());
21622
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
21623
+ return new RulesyncCommand({
21624
+ relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
21625
+ relativeFilePath: `${slug}.md`,
21626
+ frontmatter: { description },
21627
+ body: body.trimStart()
21628
+ });
21629
+ }
21630
+ static fromRulesyncCommand({ outputRoot, rulesyncCommand, global = false }) {
21631
+ const paths = KimiCodeCommand.getSettablePaths({ global });
21632
+ return new KimiCodeCommand({
21633
+ outputRoot,
21634
+ relativeDirPath: paths.relativeDirPath,
21635
+ relativeFilePath: SKILL_FILE_NAME,
21636
+ slug: commandSlug(rulesyncCommand.getRelativeFilePath()),
21637
+ fileContent: commandSkillContent$1(rulesyncCommand)
21638
+ });
21639
+ }
21640
+ getFileContent() {
21641
+ return this.fileContent;
21642
+ }
21643
+ };
21644
+ //#endregion
21433
21645
  //#region src/features/commands/kimi-code-plugin-command.ts
21434
21646
  /**
21435
21647
  * Kimi Code reads only `name` and `description` from a plugin command. The
@@ -21584,6 +21796,14 @@ const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
21584
21796
  const KIRO_MCP_FILE_NAME = "mcp.json";
21585
21797
  const KIRO_IGNORE_FILE_NAME = ".kiroignore";
21586
21798
  const KIRO_GLOBAL_IGNORE_FILE_NAME = "kiroignore";
21799
+ /**
21800
+ * Kiro's capability-based permission rules (IDE 1.0, CLI V3). The user-level
21801
+ * file lives at `~/.kiro/settings/permissions.yaml`; the workspace-level file
21802
+ * sits under a hash-derived `~/.kiro/workspace-roots/<hash>/` directory outside
21803
+ * the repository, so only the user-level file is generated.
21804
+ * @see https://kiro.dev/docs/permissions/
21805
+ */
21806
+ const KIRO_PERMISSIONS_FILE_NAME = "permissions.yaml";
21587
21807
  //#endregion
21588
21808
  //#region src/features/commands/kiro-command.ts
21589
21809
  var KiroCommand = class KiroCommand extends ToolCommand {
@@ -23596,6 +23816,193 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23596
23816
  }
23597
23817
  };
23598
23818
  //#endregion
23819
+ //#region src/constants/trae-paths.ts
23820
+ /**
23821
+ * Trae (ByteDance's AI IDE, "TraeCode" in its current docs) keeps project
23822
+ * configuration under `.trae/` and user configuration under `~/.trae/`.
23823
+ *
23824
+ * @see https://docs.trae.ai/ide/rules?_lang=en
23825
+ * @see https://docs.trae.ai/ide/skills?_lang=en
23826
+ * @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
23827
+ * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23828
+ * @see https://docs.trae.ai/ide/ignore-files?_lang=en
23829
+ */
23830
+ const TRAE_DIR = ".trae";
23831
+ /** Project MCP servers (`mcpServers` object), project scope only. */
23832
+ const TRAE_MCP_FILE_NAME = "mcp.json";
23833
+ /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
23834
+ const TRAE_SKILLS_DIR_PATH = join(TRAE_DIR, "skills");
23835
+ /**
23836
+ * Custom commands: Markdown files under `.trae/commands/` (project, up to three nested
23837
+ * directory levels) and `~/.trae/commands/` (global).
23838
+ */
23839
+ const TRAE_COMMANDS_DIR_PATH = join(TRAE_DIR, "commands");
23840
+ /** Indexing ignore list (gitignore syntax), `.trae/.ignore`, project scope only. */
23841
+ const TRAE_IGNORE_FILE_NAME = ".ignore";
23842
+ //#endregion
23843
+ //#region src/features/commands/trae-command.ts
23844
+ const TraeCommandFrontmatterSchema = z.looseObject({
23845
+ description: z.optional(z.string()),
23846
+ name: z.optional(z.string())
23847
+ });
23848
+ /**
23849
+ * Trae reads project commands only up to three directory levels below
23850
+ * `.trae/commands/`; a command file nested deeper is never picked up.
23851
+ */
23852
+ const TRAE_MAX_PROJECT_COMMAND_DIR_DEPTH = 3;
23853
+ /**
23854
+ * Custom command for Trae.
23855
+ *
23856
+ * Trae reads Markdown command files under `.trae/commands/` (project, up to
23857
+ * three levels of nested directories) and `~/.trae/commands/` (global). The
23858
+ * YAML frontmatter carries `name` and `description`, and the body below the
23859
+ * closing `---` holds the instructions. rulesync writes the rulesync
23860
+ * `description`, a `name` equal to the file name stem (as Trae's editor does),
23861
+ * plus any fields of the `trae` section (which win over the derived ones), and
23862
+ * keeps nested command paths.
23863
+ *
23864
+ * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23865
+ */
23866
+ var TraeCommand = class TraeCommand extends ToolCommand {
23867
+ frontmatter;
23868
+ body;
23869
+ constructor({ frontmatter, body, ...rest }) {
23870
+ if (rest.validate) {
23871
+ const result = TraeCommandFrontmatterSchema.safeParse(frontmatter);
23872
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
23873
+ }
23874
+ super({
23875
+ ...rest,
23876
+ fileContent: stringifyFrontmatter(body, frontmatter)
23877
+ });
23878
+ this.frontmatter = frontmatter;
23879
+ this.body = body;
23880
+ }
23881
+ static getSettablePaths(_options = {}) {
23882
+ return { relativeDirPath: TRAE_COMMANDS_DIR_PATH };
23883
+ }
23884
+ getBody() {
23885
+ return this.body;
23886
+ }
23887
+ getFrontmatter() {
23888
+ return this.frontmatter;
23889
+ }
23890
+ toRulesyncCommand() {
23891
+ const { description, name, ...otherFields } = this.frontmatter;
23892
+ const restFields = name === void 0 || name === basename(this.relativeFilePath, ".md") ? otherFields : {
23893
+ name,
23894
+ ...otherFields
23895
+ };
23896
+ const rulesyncFrontmatter = {
23897
+ targets: ["*"],
23898
+ description,
23899
+ ...Object.keys(restFields).length > 0 && { trae: restFields }
23900
+ };
23901
+ return new RulesyncCommand({
23902
+ outputRoot: ".",
23903
+ frontmatter: rulesyncFrontmatter,
23904
+ body: this.body,
23905
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
23906
+ relativeFilePath: this.relativeFilePath,
23907
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
23908
+ validate: true
23909
+ });
23910
+ }
23911
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
23912
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
23913
+ const { name: _name, ...traeFields } = rulesyncFrontmatter.trae ?? {};
23914
+ const traeFrontmatter = {
23915
+ name: this.deriveName(rulesyncCommand),
23916
+ description: rulesyncFrontmatter.description,
23917
+ ...traeFields
23918
+ };
23919
+ return new TraeCommand({
23920
+ outputRoot,
23921
+ frontmatter: traeFrontmatter,
23922
+ body: rulesyncCommand.getBody(),
23923
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
23924
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
23925
+ validate
23926
+ });
23927
+ }
23928
+ validate() {
23929
+ if (!this.frontmatter) return {
23930
+ success: true,
23931
+ error: null
23932
+ };
23933
+ const result = TraeCommandFrontmatterSchema.safeParse(this.frontmatter);
23934
+ if (result.success) return {
23935
+ success: true,
23936
+ error: null
23937
+ };
23938
+ return {
23939
+ success: false,
23940
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
23941
+ };
23942
+ }
23943
+ /**
23944
+ * Warn about commands Trae will not load as written: Trae documents `name`
23945
+ * as the command's unique identifier, so two commands deriving the same
23946
+ * `name` (e.g. `a/review.md` and `b/review.md`) collide, and it reads
23947
+ * project commands only up to three nested directory levels. Both files are
23948
+ * still written, so this warns rather than failing the run.
23949
+ */
23950
+ static validateRulesyncCommands({ rulesyncCommands, logger, global = false }) {
23951
+ const pathsByName = /* @__PURE__ */ new Map();
23952
+ for (const rulesyncCommand of rulesyncCommands) {
23953
+ if (!this.isTargetedByRulesyncCommand(rulesyncCommand)) continue;
23954
+ const relativeFilePath = toPosixPath(rulesyncCommand.getRelativeFilePath());
23955
+ const name = this.deriveName(rulesyncCommand);
23956
+ if (typeof name === "string") pathsByName.set(name, [...pathsByName.get(name) ?? [], relativeFilePath]);
23957
+ const dirDepth = relativeFilePath.split("/").length - 1;
23958
+ 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.`);
23959
+ }
23960
+ for (const [name, paths] of pathsByName) {
23961
+ if (paths.length < 2) continue;
23962
+ 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.`);
23963
+ }
23964
+ }
23965
+ /**
23966
+ * The `name` Trae sees for a command: the `trae` section's `name` when set,
23967
+ * otherwise the file name stem. A non-string override is returned as is so
23968
+ * frontmatter validation still rejects it.
23969
+ */
23970
+ static deriveName(rulesyncCommand) {
23971
+ return rulesyncCommand.getFrontmatter().trae?.name ?? basename(rulesyncCommand.getRelativeFilePath(), ".md");
23972
+ }
23973
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
23974
+ return this.isTargetedByRulesyncCommandDefault({
23975
+ rulesyncCommand,
23976
+ toolTarget: "trae"
23977
+ });
23978
+ }
23979
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
23980
+ const paths = this.getSettablePaths({ global });
23981
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
23982
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
23983
+ const result = TraeCommandFrontmatterSchema.safeParse(frontmatter);
23984
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
23985
+ return new TraeCommand({
23986
+ outputRoot,
23987
+ relativeDirPath: paths.relativeDirPath,
23988
+ relativeFilePath,
23989
+ frontmatter: result.data,
23990
+ body: body.trim(),
23991
+ validate
23992
+ });
23993
+ }
23994
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
23995
+ return new TraeCommand({
23996
+ outputRoot,
23997
+ relativeDirPath,
23998
+ relativeFilePath,
23999
+ frontmatter: { description: "" },
24000
+ body: "",
24001
+ validate: false
24002
+ });
24003
+ }
24004
+ };
24005
+ //#endregion
23599
24006
  //#region src/constants/warp-paths.ts
23600
24007
  const WARP_DIR = ".warp";
23601
24008
  const WARP_SKILLS_DIR_PATH = join(WARP_DIR, "skills");
@@ -24146,6 +24553,17 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24146
24553
  supportsSubdirectory: false
24147
24554
  }
24148
24555
  }],
24556
+ ["kimi-code", {
24557
+ class: KimiCodeCommand,
24558
+ meta: {
24559
+ extension: "md",
24560
+ supportsProject: true,
24561
+ supportsGlobal: true,
24562
+ isSimulated: false,
24563
+ supportsSubdirectory: false,
24564
+ skipToolFileScan: true
24565
+ }
24566
+ }],
24149
24567
  ["kimi-code-plugin", {
24150
24568
  class: KimiCodePluginCommand,
24151
24569
  meta: {
@@ -24306,6 +24724,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24306
24724
  supportsSubdirectory: false
24307
24725
  }
24308
24726
  }],
24727
+ ["trae", {
24728
+ class: TraeCommand,
24729
+ meta: {
24730
+ extension: "md",
24731
+ supportsProject: true,
24732
+ supportsGlobal: true,
24733
+ isSimulated: false,
24734
+ supportsSubdirectory: true
24735
+ }
24736
+ }],
24309
24737
  ["devin", {
24310
24738
  class: DevinCommand,
24311
24739
  meta: {
@@ -24389,7 +24817,8 @@ var CommandsProcessor = class extends FeatureProcessor {
24389
24817
  await factory.class.validateRulesyncCommands?.({
24390
24818
  inputRoots: this.inputRoots,
24391
24819
  rulesyncCommands,
24392
- logger: this.logger
24820
+ logger: this.logger,
24821
+ global: this.global
24393
24822
  });
24394
24823
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
24395
24824
  const toolCommands = [];
@@ -26069,8 +26498,8 @@ function isSupportedHookType({ type, converterConfig }) {
26069
26498
  * side either way, so the value is dropped rather than carried into a config
26070
26499
  * the tool would ignore or refuse.
26071
26500
  */
26072
- function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }) {
26073
- const { canonical, tool, commandOnly, events } = spec;
26501
+ function isKeywordFieldApplicable({ spec, hookType, eventName, flags, direction, warn }) {
26502
+ const { canonical, tool, commandOnly, hookTypes, events, ignoredEvents, ignoredWhenFlags } = spec;
26074
26503
  const field = direction === "generated" ? canonical : tool;
26075
26504
  if (!isFieldApplicable({
26076
26505
  commandOnly,
@@ -26079,10 +26508,23 @@ function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }
26079
26508
  warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not ${direction}.`);
26080
26509
  return false;
26081
26510
  }
26511
+ if (hookTypes !== void 0 && !hookTypes.has(hookType)) {
26512
+ 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}.`);
26513
+ return false;
26514
+ }
26082
26515
  if (events !== void 0 && !events.has(eventName)) {
26083
26516
  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}.`);
26084
26517
  return false;
26085
26518
  }
26519
+ if (ignoredEvents?.has(eventName)) {
26520
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool ignores "${tool}" on "${eventName}", so it is not ${direction}.`);
26521
+ return false;
26522
+ }
26523
+ const setFlag = hookType === "command" ? ignoredWhenFlags?.find((flag) => flags[flag] === true) : void 0;
26524
+ if (setFlag !== void 0) {
26525
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool ignores "${tool}" on a hook that sets "${setFlag}", so it is not ${direction}.`);
26526
+ return false;
26527
+ }
26086
26528
  return true;
26087
26529
  }
26088
26530
  /**
@@ -26099,6 +26541,7 @@ function emitKeywordFields({ def, hookType, eventName, fields, warn }) {
26099
26541
  spec,
26100
26542
  hookType,
26101
26543
  eventName,
26544
+ flags: def,
26102
26545
  direction: "generated",
26103
26546
  warn
26104
26547
  })) emitted[spec.tool] = spec.trueValue;
@@ -26120,6 +26563,7 @@ function importKeywordFields({ h, hookType, eventName, fields, warn }) {
26120
26563
  spec,
26121
26564
  hookType,
26122
26565
  eventName,
26566
+ flags: h,
26123
26567
  direction: "imported",
26124
26568
  warn
26125
26569
  })) continue;
@@ -28012,6 +28456,20 @@ const CLAUDE_SPEC = {
28012
28456
  tool: "continueOnBlock"
28013
28457
  }
28014
28458
  ],
28459
+ keywordPassthroughFields: [{
28460
+ canonical: "failClosed",
28461
+ tool: "onFailure",
28462
+ trueValue: "block",
28463
+ falseValue: "continue",
28464
+ hookTypes: /* @__PURE__ */ new Set(["command", "http"]),
28465
+ ignoredEvents: /* @__PURE__ */ new Set([
28466
+ "stop",
28467
+ "subagentStop",
28468
+ "taskCompleted",
28469
+ "teammateIdle"
28470
+ ]),
28471
+ ignoredWhenFlags: ["async", "asyncRewake"]
28472
+ }],
28015
28473
  arrayPassthroughFields: [{
28016
28474
  canonical: "args",
28017
28475
  tool: "args",
@@ -28734,18 +29192,20 @@ const CODEXCLI_CONVERTER_CONFIG = {
28734
29192
  canonicalToToolEventNames: CANONICAL_TO_CODEXCLI_EVENT_NAMES,
28735
29193
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
28736
29194
  projectDirVar: "",
28737
- supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
29195
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "mcp_tool"]),
28738
29196
  passthroughFields: ["name", "description"],
28739
29197
  stringPassthroughFields: [{
28740
29198
  canonical: "commandWindows",
28741
- tool: "commandWindows"
29199
+ tool: "commandWindows",
29200
+ commandOnly: true
28742
29201
  }, {
28743
29202
  canonical: "statusMessage",
28744
29203
  tool: "statusMessage"
28745
29204
  }],
28746
29205
  numberPassthroughFields: [{
28747
29206
  canonical: "additionalContextLimit",
28748
- tool: "additionalContextLimit"
29207
+ tool: "additionalContextLimit",
29208
+ commandOnly: true
28749
29209
  }],
28750
29210
  booleanPassthroughFields: [{
28751
29211
  canonical: "async",
@@ -28754,6 +29214,74 @@ const CODEXCLI_CONVERTER_CONFIG = {
28754
29214
  }]
28755
29215
  };
28756
29216
  /**
29217
+ * Whether a JSON value holds a `null` anywhere. Codex rejects an `mcp_tool`
29218
+ * hook `input` that cannot be represented as TOML (it hashes the handler as
29219
+ * TOML for trust review), and `null` is the one JSON value TOML lacks.
29220
+ */
29221
+ function containsNull(value) {
29222
+ if (value === null) return true;
29223
+ if (Array.isArray(value)) return value.some(containsNull);
29224
+ if (typeof value === "object") return Object.values(value).some(containsNull);
29225
+ return false;
29226
+ }
29227
+ const WHOLE_FILE_FAILURE = "Codex fails to load the whole hooks file on such a handler";
29228
+ /**
29229
+ * Why this generated handler must not be written, or `undefined` when Codex
29230
+ * can load and run it. Codex deserializes `timeout` as `Option<u64>` on every
29231
+ * handler type, and `server` / `tool` as required strings plus `input` as a
29232
+ * TOML-representable map on `mcp_tool`; a handler that fails that stops the
29233
+ * whole hooks file from loading rather than just that one hook. An empty
29234
+ * `server` / `tool` string still loads but can never resolve to a tool.
29235
+ * https://github.com/openai/codex/blob/main/codex-rs/config/src/hook_config.rs
29236
+ */
29237
+ function describeRejectedHandler(handler) {
29238
+ const { timeout } = handler;
29239
+ 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}`;
29240
+ if (handler.type !== "mcp_tool") return;
29241
+ for (const field of ["server", "tool"]) {
29242
+ const value = handler[field];
29243
+ if (typeof value !== "string") return `it has no "${field}", and ${WHOLE_FILE_FAILURE}`;
29244
+ if (value === "") return `its "${field}" is empty, so it can never resolve to an MCP tool`;
29245
+ }
29246
+ if (containsNull(handler.input)) return `its "input" holds a null, which Codex cannot represent as TOML, and ${WHOLE_FILE_FAILURE}`;
29247
+ }
29248
+ /**
29249
+ * Drop the generated handlers Codex would refuse to load (or could never
29250
+ * run), and warn about the `mcp_tool` ones Codex loads but skips (`SessionEnd`
29251
+ * MCP hooks are skipped with a startup warning). Matcher groups left without
29252
+ * handlers are removed. https://learn.chatgpt.com/docs/hooks
29253
+ */
29254
+ function filterCodexHandlers({ hooks, logger }) {
29255
+ const result = {};
29256
+ const skippedSessionEndTools = [];
29257
+ for (const [eventName, groups] of Object.entries(hooks)) {
29258
+ const kept = [];
29259
+ for (const group of groups) {
29260
+ const entry = group;
29261
+ if (!Array.isArray(entry.hooks)) {
29262
+ kept.push(group);
29263
+ continue;
29264
+ }
29265
+ const handlers = entry.hooks.filter((handler) => {
29266
+ const reason = describeRejectedHandler(handler);
29267
+ if (reason !== void 0) {
29268
+ logger?.warn(`Skipping a "${String(handler.type)}" hook on Codex CLI "${eventName}": ${reason}.`);
29269
+ return false;
29270
+ }
29271
+ if (eventName === "SessionEnd" && handler.type === "mcp_tool") skippedSessionEndTools.push(`${String(handler.server)}/${String(handler.tool)}`);
29272
+ return true;
29273
+ });
29274
+ if (handlers.length > 0) kept.push({
29275
+ ...entry,
29276
+ hooks: handlers
29277
+ });
29278
+ }
29279
+ if (kept.length > 0) result[eventName] = kept;
29280
+ }
29281
+ 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(", ")}.`);
29282
+ return result;
29283
+ }
29284
+ /**
28757
29285
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
28758
29286
  * Reads the existing file (if any), parses TOML, and returns the content without writing to
28759
29287
  * disk. The caller is responsible for writing via the normal write phase.
@@ -28846,10 +29374,13 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
28846
29374
  const config = rulesyncHooks.getJson();
28847
29375
  const merged = mergeGeneratedHookLists({
28848
29376
  existingContent,
28849
- generatedHooks: canonicalToToolHooks({
28850
- config,
28851
- toolOverrideHooks: config.codexcli?.hooks,
28852
- converterConfig: CODEXCLI_CONVERTER_CONFIG,
29377
+ generatedHooks: filterCodexHandlers({
29378
+ hooks: canonicalToToolHooks({
29379
+ config,
29380
+ toolOverrideHooks: config.codexcli?.hooks,
29381
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
29382
+ logger
29383
+ }),
28853
29384
  logger
28854
29385
  }),
28855
29386
  shape: "matcher-groups",
@@ -32028,60 +32559,6 @@ var KiloHooks = class KiloHooks extends ToolHooks {
32028
32559
  }
32029
32560
  };
32030
32561
  //#endregion
32031
- //#region src/constants/kimi-code-paths.ts
32032
- const KIMI_CODE_DIR = ".kimi-code";
32033
- const KIMI_CODE_RULE_FILE_NAME = "AGENTS.md";
32034
- const KIMI_CODE_MCP_FILE_NAME = "mcp.json";
32035
- const KIMI_CODE_CONFIG_FILE_NAME = "config.toml";
32036
- const KIMI_CODE_SKILLS_DIR_NAME = "skills";
32037
- const KIMI_CODE_AGENTS_DIR_NAME = "agents";
32038
- const KIMI_CODE_SKILLS_DIR_PATH = join(KIMI_CODE_DIR, KIMI_CODE_SKILLS_DIR_NAME);
32039
- join(KIMI_CODE_DIR, KIMI_CODE_AGENTS_DIR_NAME);
32040
- const KIMI_CODE_SHARED_SKILLS_DIR_PATH = join(".agents", "skills");
32041
- const KIMI_CODE_SHARED_AGENTS_DIR_PATH = join(".agents", "agents");
32042
- //#endregion
32043
- //#region src/utils/kimi-code.ts
32044
- function getKimiCodeHome() {
32045
- const configuredHome = process.env.KIMI_CODE_HOME?.trim();
32046
- return configuredHome ? resolve(configuredHome) : void 0;
32047
- }
32048
- function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
32049
- return global && getKimiCodeHome() ? relativeDirPath : join(KIMI_CODE_DIR, relativeDirPath);
32050
- }
32051
- /**
32052
- * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
32053
- * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
32054
- * Declared unconditionally so the derived shared-file keys — and the drift
32055
- * guards checked against them — do not depend on the ambient environment.
32056
- */
32057
- function getKimiCodeSharedConfigWritePaths() {
32058
- return [{
32059
- relativeDirPath: KIMI_CODE_DIR,
32060
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32061
- }, {
32062
- relativeDirPath: ".",
32063
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32064
- }];
32065
- }
32066
- /**
32067
- * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
32068
- * Both spellings carry the same declaration, but passing the key of the file
32069
- * being written keeps the write path and the drift guards on the same entry.
32070
- */
32071
- function getKimiCodeConfigSharedFileKey({ global }) {
32072
- return sharedConfigFileKey({
32073
- relativeDirPath: getKimiCodeRelativeDirPath({ global }),
32074
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
32075
- });
32076
- }
32077
- function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
32078
- return getToolRulesyncOutputRoot({
32079
- nativeOutputRoot,
32080
- global,
32081
- toolHome: getKimiCodeHome
32082
- });
32083
- }
32084
- //#endregion
32085
32562
  //#region src/features/hooks/kimi-code-hooks.ts
32086
32563
  function runFromTrustedDirectory({ command, trustedDirectory }) {
32087
32564
  if (process.platform === "win32") return `set "RULESYNC_KIMI_HOOK_CWD=1" && cd /d "${trustedDirectory.replaceAll("%", "%%").replaceAll("\"", "\"\"")}" && ${command}`;
@@ -35538,7 +36015,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
35538
36015
  supportsImport: true
35539
36016
  },
35540
36017
  supportedEvents: CODEXCLI_HOOK_EVENTS,
35541
- supportedHookTypes: ["command"],
36018
+ supportedHookTypes: ["command", "mcp_tool"],
35542
36019
  supportsMatcher: true
35543
36020
  }],
35544
36021
  ["commandcode", {
@@ -37800,6 +38277,23 @@ function toReasonixTable(value) {
37800
38277
  if (!isPlainObject$1(value)) return {};
37801
38278
  return { ...value };
37802
38279
  }
38280
+ /**
38281
+ * The tool name a Reasonix `[permissions]` rule must use to reach file reads.
38282
+ * Reasonix matches a rule's tool exactly against its registered tools
38283
+ * (`read_file`, `write_file`, `grep`, ...); only `Bash`, `Edit`, `Browser` and
38284
+ * `Computer` are Claude-Code-style aliases (`canonicalRuleTool` in
38285
+ * `internal/safety/permission/permission.go`), so a `Read(...)` rule matches
38286
+ * nothing. Shared by the `ignore` and `permissions` adapters, which both write
38287
+ * read denies into the same table.
38288
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.41.0/internal/safety/permission/permission.go
38289
+ */
38290
+ const REASONIX_READ_TOOL_NAME = "read_file";
38291
+ /**
38292
+ * The spelling rulesync wrote for read rules before it followed the Reasonix
38293
+ * tool vocabulary. Still accepted on import, and an entry spelled this way is
38294
+ * treated as rulesync-owned on generate so the dormant copy is replaced.
38295
+ */
38296
+ const REASONIX_LEGACY_READ_TOOL_NAME = "Read";
37803
38297
  //#endregion
37804
38298
  //#region src/features/ignore/reasonix-ignore.ts
37805
38299
  const permissionsTableOf = (document) => toReasonixTable(document.permissions);
@@ -37818,10 +38312,15 @@ const asClaudeStyleSettings = (document) => {
37818
38312
  ...table,
37819
38313
  allow: toReasonixStringArray(table.allow),
37820
38314
  ask: toReasonixStringArray(table.ask),
37821
- deny: toReasonixStringArray(table.deny)
38315
+ deny: toReasonixStringArray(table.deny).filter((entry) => !isReadDenyEntry(entry, REASONIX_LEGACY_READ_TOOL_NAME))
37822
38316
  }
37823
38317
  };
37824
38318
  };
38319
+ /** The pattern of a `read_file(...)` (or legacy `Read(...)`) deny, else null. */
38320
+ const readDenyPatternOf = (entry) => {
38321
+ for (const toolName of [REASONIX_READ_TOOL_NAME, REASONIX_LEGACY_READ_TOOL_NAME]) if (isReadDenyEntry(entry, toolName)) return entry.slice(toolName.length + 1, -1);
38322
+ return null;
38323
+ };
37825
38324
  /**
37826
38325
  * Drop a `[permissions]` table that ended up with nothing in it, so an empty
37827
38326
  * `.rulesyncignore` does not add a bare table header to a file that never had
@@ -37834,13 +38333,16 @@ const withoutEmptyPermissions = (settings) => {
37834
38333
  return document;
37835
38334
  };
37836
38335
  /**
37837
- * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
38336
+ * Writes `.rulesyncignore` patterns as `read_file(<pattern>)` entries in the
37838
38337
  * `[permissions] deny` table of `reasonix.toml` (project) /
37839
38338
  * `~/.reasonix/config.toml` (global).
37840
38339
  *
37841
38340
  * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
37842
38341
  * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
37843
38342
  * while `forbid_read` is documented as absolute paths with no glob support.
38343
+ * The tool is spelled `read_file` because Reasonix matches rule tools against
38344
+ * its registered tool names (see {@link REASONIX_READ_TOOL_NAME}); a legacy
38345
+ * `Read(...)` deny is still read back on import.
37844
38346
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
37845
38347
  */
37846
38348
  var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
@@ -37867,7 +38369,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
37867
38369
  return false;
37868
38370
  }
37869
38371
  toRulesyncIgnore() {
37870
- const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
38372
+ const rulesyncPatterns = uniq(this.patterns.map((entry) => readDenyPatternOf(entry)).filter((pattern) => pattern !== null && pattern.length > 0));
37871
38373
  return new RulesyncIgnore({
37872
38374
  outputRoot: this.outputRoot,
37873
38375
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
@@ -37876,7 +38378,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
37876
38378
  });
37877
38379
  }
37878
38380
  static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
37879
- const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
38381
+ 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));
37880
38382
  const paths = this.getSettablePaths({ global });
37881
38383
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
37882
38384
  const existingDocument = parseSharedConfig({
@@ -37886,7 +38388,8 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
37886
38388
  });
37887
38389
  const document = withoutEmptyPermissions(applyIgnoreReadDenies({
37888
38390
  settings: asClaudeStyleSettings(existingDocument),
37889
- readDenies
38391
+ readDenies,
38392
+ readToolName: REASONIX_READ_TOOL_NAME
37890
38393
  }));
37891
38394
  return new ReasonixIgnore({
37892
38395
  outputRoot,
@@ -38031,6 +38534,61 @@ var TabnineIgnore = class TabnineIgnore extends ToolIgnore {
38031
38534
  }
38032
38535
  };
38033
38536
  //#endregion
38537
+ //#region src/features/ignore/trae-ignore.ts
38538
+ /**
38539
+ * Trae ignore file implementation.
38540
+ *
38541
+ * Trae reads `.trae/.ignore` (gitignore syntax) as a supplement to
38542
+ * `.gitignore` when it indexes the codebase; ignored files are also left out
38543
+ * of `#Workspace` / `#Folder` context. There is no user-level ignore file, so
38544
+ * only project scope is supported.
38545
+ *
38546
+ * @see https://docs.trae.ai/ide/ignore-files?_lang=en
38547
+ */
38548
+ var TraeIgnore = class TraeIgnore extends ToolIgnore {
38549
+ static getSettablePaths(_params = {}) {
38550
+ return {
38551
+ relativeDirPath: TRAE_DIR,
38552
+ relativeFilePath: TRAE_IGNORE_FILE_NAME
38553
+ };
38554
+ }
38555
+ toRulesyncIgnore() {
38556
+ return this.toRulesyncIgnoreDefault();
38557
+ }
38558
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
38559
+ const paths = this.getSettablePaths({ global });
38560
+ return new TraeIgnore({
38561
+ outputRoot,
38562
+ relativeDirPath: paths.relativeDirPath,
38563
+ relativeFilePath: paths.relativeFilePath,
38564
+ fileContent: rulesyncIgnore.getFileContent(),
38565
+ global
38566
+ });
38567
+ }
38568
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
38569
+ const { relativeDirPath, relativeFilePath } = this.getSettablePaths({ global });
38570
+ const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
38571
+ return new TraeIgnore({
38572
+ outputRoot,
38573
+ relativeDirPath,
38574
+ relativeFilePath,
38575
+ fileContent,
38576
+ validate,
38577
+ global
38578
+ });
38579
+ }
38580
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
38581
+ return new TraeIgnore({
38582
+ outputRoot,
38583
+ relativeDirPath,
38584
+ relativeFilePath,
38585
+ fileContent: "",
38586
+ validate: false,
38587
+ global
38588
+ });
38589
+ }
38590
+ };
38591
+ //#endregion
38034
38592
  //#region src/features/ignore/vibe-ignore.ts
38035
38593
  var VibeIgnore = class VibeIgnore extends ToolIgnore {
38036
38594
  static getSettablePaths() {
@@ -38259,6 +38817,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
38259
38817
  ["zoocode", { class: RooIgnore }],
38260
38818
  ["devin", { class: DevinIgnore }],
38261
38819
  ["tabnine", { class: TabnineIgnore }],
38820
+ ["trae", { class: TraeIgnore }],
38262
38821
  ["vibe", { class: VibeIgnore }],
38263
38822
  ["warp", { class: WarpIgnore }],
38264
38823
  ["zed", { class: ZedIgnore }]
@@ -43611,12 +44170,14 @@ function toGitlabduoTimeout({ serverName, server, logger }) {
43611
44170
  /**
43612
44171
  * Read GitLab Duo's `timeoutMs` back into the canonical `timeout`
43613
44172
  * (milliseconds). A server that already carries a `timeout` is left as is, so
43614
- * both keys round-trip and `timeoutMs` keeps winning on the next generate.
44173
+ * both keys round-trip and `timeoutMs` keeps winning on the next generate. A
44174
+ * non-number `timeoutMs` is not copied, since the canonical `timeout` must be a
44175
+ * number; it is kept verbatim as a passthrough key instead.
43615
44176
  */
43616
44177
  function fromGitlabduoTimeout(server) {
43617
44178
  if (typeof server !== "object" || server === null || Array.isArray(server)) return server;
43618
44179
  const { timeoutMs, ...rest } = server;
43619
- if (timeoutMs === void 0 || rest.timeout !== void 0) return server;
44180
+ if (typeof timeoutMs !== "number" || rest.timeout !== void 0) return server;
43620
44181
  return {
43621
44182
  ...rest,
43622
44183
  timeout: timeoutMs
@@ -45331,7 +45892,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
45331
45892
  feature: "mcp",
45332
45893
  existingContent: fileContent ?? "",
45333
45894
  patch: {
45334
- mcp: convertedMcp,
45895
+ mcp: undefinedWhenEmpty(convertedMcp),
45335
45896
  ...buildKiloMcpPermissionPatch({
45336
45897
  existing: existingDocument,
45337
45898
  managedServerNames: uniq([...Object.keys(mcpServers), ...Object.keys(isRecord$1(existingDocument.mcp) ? existingDocument.mcp : {})]),
@@ -46191,7 +46752,7 @@ var OpencodeMcp = class extends ToolMcp {
46191
46752
  feature: "mcp",
46192
46753
  existingContent: fileContent ?? "",
46193
46754
  patch: {
46194
- mcp: convertedMcp,
46755
+ mcp: undefinedWhenEmpty(convertedMcp),
46195
46756
  tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
46196
46757
  },
46197
46758
  filePath: join(jsonDir, relativeFilePath),
@@ -46539,9 +47100,12 @@ function convertFromMusecodeFormat(musecodeMcp) {
46539
47100
  /**
46540
47101
  * Meta Muse Code MCP servers.
46541
47102
  *
46542
- * Muse Code reads MCP servers only from the `mcp_servers` (or `mcpServers`)
46543
- * block of the GLOBAL user settings file `~/.config/muse/settings.json`; no
46544
- * project-scoped MCP location is documented. The settings file must carry
47103
+ * rulesync writes Muse Code MCP servers only to the `mcp_servers` (or
47104
+ * `mcpServers`) block of the GLOBAL user settings file
47105
+ * `~/.config/muse/settings.json`, the location extending.md documents. Muse
47106
+ * Code 1.4.0+ also loads a trusted project `.mcp.json` (changelog only), which
47107
+ * is not emitted here because the `claudecode` target already writes that
47108
+ * file. The settings file must carry
46545
47109
  * `"schema_version": 1` — a file that omits that key fails every command at
46546
47110
  * startup with `malformed settings file` — so the key is bootstrapped when the
46547
47111
  * file is created and preserved when it already exists. Other settings keys are
@@ -46549,6 +47113,7 @@ function convertFromMusecodeFormat(musecodeMcp) {
46549
47113
  *
46550
47114
  * @see https://dev.meta.ai/docs/muse-code/configuration.md
46551
47115
  * @see https://dev.meta.ai/docs/muse-code/extending.md
47116
+ * @see https://dev.meta.ai/docs/muse-code/changelog
46552
47117
  */
46553
47118
  var MusecodeMcp = class MusecodeMcp extends ToolMcp {
46554
47119
  json;
@@ -47089,8 +47654,8 @@ function convertFromPiFormat(mcpServers) {
47089
47654
  * existing file (`autoEnableCodemode`, ...) are preserved. Pi expands `${VAR}`
47090
47655
  * in `env` and `headers` values itself, so those are written unchanged.
47091
47656
  *
47092
- * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md
47093
- * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/mcp-servers.ts
47657
+ * @see https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/docs/mcp.md
47658
+ * @see https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/src/core/mcp-servers.ts
47094
47659
  */
47095
47660
  var PiMcp = class PiMcp extends ToolMcp {
47096
47661
  json;
@@ -47781,7 +48346,7 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
47781
48346
  feature: "mcp",
47782
48347
  existingContent,
47783
48348
  patch: {
47784
- mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers()),
48349
+ mcpServers: undefinedWhenEmpty(convertToQwencodeFormat(rulesyncMcp.getMcpServers())),
47785
48350
  ...mcpPatch !== void 0 && { mcp: mcpPatch }
47786
48351
  },
47787
48352
  filePath
@@ -49204,21 +49769,6 @@ function deriveTransportAllowlist(servers) {
49204
49769
  return allowlist;
49205
49770
  }
49206
49771
  //#endregion
49207
- //#region src/constants/trae-paths.ts
49208
- /**
49209
- * Trae (ByteDance's AI IDE, "TraeCode" in its current docs) keeps project
49210
- * configuration under `.trae/` and user configuration under `~/.trae/`.
49211
- *
49212
- * @see https://docs.trae.ai/ide/rules?_lang=en
49213
- * @see https://docs.trae.ai/ide/skills?_lang=en
49214
- * @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
49215
- */
49216
- const TRAE_DIR = ".trae";
49217
- /** Project MCP servers (`mcpServers` object), project scope only. */
49218
- const TRAE_MCP_FILE_NAME = "mcp.json";
49219
- /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
49220
- const TRAE_SKILLS_DIR_PATH = join(TRAE_DIR, "skills");
49221
- //#endregion
49222
49772
  //#region src/features/mcp/trae-mcp.ts
49223
49773
  /** A `${...}` reference Trae does not expand: anything but `${workspaceFolder}`. */
49224
49774
  const UNEXPANDED_VARIABLE_REGEX = /\$\{(?!workspaceFolder\})[^}]+\}/;
@@ -51828,6 +52378,11 @@ function parsedGlobsIntersect(left, right, budget) {
51828
52378
  * @see https://antigravity.google/docs/permissions?tab=cli
51829
52379
  */
51830
52380
  const REGEX_PREFIX = "regex:";
52381
+ /**
52382
+ * The Antigravity actions whose target is a command: `command`, and
52383
+ * `unsandboxed`, which takes the same word-by-word prefix or `regex:` target.
52384
+ */
52385
+ const ANTIGRAVITY_COMMAND_ACTIONS = /* @__PURE__ */ new Set(["command", "unsandboxed"]);
51831
52386
  const TRAILING_ANCHOR = /(?<!\\)\$$/;
51832
52387
  const PLAIN_TARGET_GLOB_CHARACTERS = /[*?[{\\]/;
51833
52388
  const REGEX_METACHARACTERS$1 = /[\\^$.|?*+()[\]{}]/;
@@ -52205,8 +52760,8 @@ function toAntigravityCommandLiteralPrefix(pattern) {
52205
52760
  }
52206
52761
  /**
52207
52762
  * Read one regex word back as a glob, or `undefined` when it uses anything a
52208
- * glob cannot say. Antigravity anchors each word anyway, so `^` and `$` are
52209
- * optional. Only escaped characters and `.*` translate; anything else keeps
52763
+ * glob cannot say. Antigravity anchors each word anyway (`^(?:pattern)$`), so
52764
+ * `^` and `$` are optional. Only escaped characters and `.*` translate; anything else keeps
52210
52765
  * the rule a regex.
52211
52766
  */
52212
52767
  function regexWordToGlob(word) {
@@ -52232,8 +52787,10 @@ function regexWordToGlob(word) {
52232
52787
  }
52233
52788
  /** Read a per-word regex back as a glob, or `undefined` when it has no glob spelling. */
52234
52789
  function regexToGlob(source) {
52790
+ const words = splitWords(source);
52791
+ if (words.length === 0) return;
52235
52792
  const globs = [];
52236
- for (const word of splitWords(source)) {
52793
+ for (const word of words) {
52237
52794
  const glob = regexWordToGlob(word);
52238
52795
  if (glob === void 0) return;
52239
52796
  globs.push(glob);
@@ -52258,36 +52815,23 @@ function fromAntigravityCommandTarget(target) {
52258
52815
  return `${target} *`;
52259
52816
  }
52260
52817
  /**
52261
- * Whether a regex word means the same anchored or not: each end is either
52262
- * anchored (`^`, an unescaped `$`) or open-ended (`.*`).
52263
- */
52264
- function isAnchoredWord(word) {
52265
- const startAnchored = word.startsWith("^") || word.startsWith(".*");
52266
- const endAnchored = TRAILING_ANCHOR.test(word) || word.endsWith(".*");
52267
- return startAnchored && endAnchored;
52268
- }
52269
- /**
52270
52818
  * Explain what importing an Antigravity `command` target may lose, or return
52271
52819
  * `undefined` when the import is faithful.
52272
52820
  *
52273
- * - A `regex:` target with a word that is not anchored is imported as a glob
52274
- * that assumes anchoring. If Antigravity matches such a word unanchored,
52275
- * the imported rule is narrower (`regex:rm` also matches `rmdir`).
52276
- * - A plain target holding characters that are special in a glob is most
52277
- * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
52278
- * Antigravity matches it literally, so it imports as a regex that matches a
52279
- * literal `*` word and is inert in the other tools it is exported to.
52821
+ * A plain target holding characters that are special in a glob is most
52822
+ * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
52823
+ * Antigravity matches it literally, so it imports as a regex that matches a
52824
+ * literal `*` word and is inert in the other tools it is exported to.
52825
+ *
52826
+ * A `regex:` word without `^...$` (`regex:rm`) is read as anchored, which is
52827
+ * faithful: Antigravity documents that each token is evaluated as the
52828
+ * anchored regex `^(?:pattern)$`.
52829
+ *
52830
+ * @see https://antigravity.google/docs/permissions
52280
52831
  */
52281
52832
  function describeAntigravityCommandImportLoss(target) {
52282
- if (target.startsWith(REGEX_PREFIX)) {
52283
- const source = target.slice(6);
52284
- if (splitWords(source).some((word) => !isAnchoredWord(word)) && regexToGlob(source) !== void 0) return {
52285
- reason: "it has a regex word without `^...$` anchors and is imported as a glob that assumes Antigravity anchors every word, so the imported rule may be narrower than the original",
52286
- denyConsequence: "The imported deny may block less than it does in Antigravity."
52287
- };
52288
- return;
52289
- }
52290
- if (target !== "*" && PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
52833
+ if (target.startsWith(REGEX_PREFIX) || target === "*") return;
52834
+ if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
52291
52835
  reason: "it holds characters that are special in a glob (`*`, `?`, `[`, `{` or `\\`), which Antigravity matches literally (likely output of an older rulesync), so it is imported as a literal-match regex that no other tool enforces",
52292
52836
  denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
52293
52837
  };
@@ -52319,17 +52863,20 @@ function importAntigravityCommandTarget({ target, entry, decision, logger, toolL
52319
52863
  * every file.
52320
52864
  * - `read_url` takes a domain and covers its subdomains. The URL path is
52321
52865
  * ignored, and `*` covers every domain.
52322
- * - `command` takes literal words or a per-word `regex:`, matched word by
52323
- * word (see `antigravity-command-patterns.ts`).
52866
+ * - `execute_url` takes a domain, like `read_url`.
52867
+ * - `command` and `unsandboxed` take literal words or a per-word `regex:`,
52868
+ * matched word by word (see `antigravity-command-patterns.ts`).
52324
52869
  *
52325
52870
  * A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
52326
52871
  * matches nothing, so a deny written that way blocks nothing. A pattern with
52327
- * no Antigravity spelling is skipped with a warning instead, except a command
52328
- * deny with leading literal words, which is widened to them.
52872
+ * no Antigravity spelling is skipped with a warning instead, except a deny
52873
+ * with a literal lead (the first words of a command, the leading directories
52874
+ * of a path), which is widened to it.
52329
52875
  *
52330
52876
  * @see https://antigravity.google/docs/permissions?tab=cli
52331
52877
  */
52332
52878
  const PATH_ACTIONS = /* @__PURE__ */ new Set(["read_file", "write_file"]);
52879
+ const URL_ACTIONS = /* @__PURE__ */ new Set(["read_url", "execute_url"]);
52333
52880
  const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
52334
52881
  "*",
52335
52882
  "**",
@@ -52341,7 +52888,8 @@ const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
52341
52888
  const DENY_HINTS = {
52342
52889
  read_file: "deny a directory or file path instead",
52343
52890
  write_file: "deny a directory or file path instead",
52344
- command: "deny the first words of the command instead"
52891
+ command: "deny the first words of the command instead",
52892
+ unsandboxed: "deny the first words of the command instead"
52345
52893
  };
52346
52894
  function hasGlob(pattern) {
52347
52895
  return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
@@ -52392,37 +52940,88 @@ function toUrlTarget(pattern) {
52392
52940
  };
52393
52941
  }
52394
52942
  function toTarget(action, pattern) {
52395
- if (action === "command") return toAntigravityCommandTarget(pattern);
52943
+ if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandTarget(pattern);
52396
52944
  if (PATH_ACTIONS.has(action)) return toPathTarget(pattern);
52397
- if (action === "read_url") return toUrlTarget(pattern);
52945
+ if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
52398
52946
  return { target: pattern };
52399
52947
  }
52400
52948
  /**
52949
+ * The leading directories of a path glob (`src` for `src/*.ts`), or
52950
+ * `undefined` when it has none. A directory target covers everything inside
52951
+ * it, so it matches at least as much as the glob. The filesystem root alone
52952
+ * (`/*`) is not a lead: a deny of it would block every file. Neither is a lead
52953
+ * with `..` in it.
52954
+ */
52955
+ function toPathLiteralPrefix(pattern) {
52956
+ const path = pattern.startsWith("./") ? pattern.slice(2) : pattern;
52957
+ const prefix = [];
52958
+ for (const segment of path.split("/").slice(0, -1)) {
52959
+ if (hasGlob(segment) || segment.includes("{") || segment.includes("}")) break;
52960
+ prefix.push(segment);
52961
+ }
52962
+ if (prefix.includes("..") || prefix.every((segment) => segment === "" || segment === ".")) return;
52963
+ return prefix.join("/");
52964
+ }
52965
+ /** The widened target of a deny that has no exact Antigravity spelling. */
52966
+ function toWidenedDenyTarget(action, pattern) {
52967
+ if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
52968
+ if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
52969
+ }
52970
+ /**
52971
+ * Warn about a path target that is a home or variable reference. Antigravity
52972
+ * documents absolute and workspace-relative paths only, so such a reference
52973
+ * may be matched literally and never apply. The entry is still written, since
52974
+ * dropping a deny would lose it where Antigravity does expand the reference.
52975
+ */
52976
+ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLabel }) {
52977
+ if (!PATH_ACTIONS.has(action) || !HOME_OR_VARIABLE_PATH.test(target)) return;
52978
+ const consequence = decision === "deny" ? "this deny may block nothing" : decision === "ask" ? `${toolLabel} may fall back to its default, so it may run without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} may fall back to its default`;
52979
+ logger.warn(`${toolLabel} permissions: ${rule}: \`~\` and environment variables may not be expanded in Antigravity path targets, so ${action}(${target}) may match nothing and ${consequence}; use an absolute path instead.`);
52980
+ }
52981
+ /**
52401
52982
  * Build the entry for one canonical rule, or return `undefined` when
52402
- * Antigravity cannot express it. A command deny is widened to its leading
52403
- * literal words rather than skipped: Antigravity applies deny over ask and
52404
- * allow, so an overlapping allow that is translated (`git push *`) cannot let
52405
- * through what the deny (`git push * --force`) was meant to block. A skipped
52406
- * deny is warned about loudly, because nothing enforces it.
52983
+ * Antigravity cannot express it. A deny is widened to its literal lead rather
52984
+ * than skipped (`git push * --force` → `command(git push)`, `src/*.ts` →
52985
+ * `read_file(src)`): Antigravity applies deny over ask and allow, so an
52986
+ * overlapping allow that is translated (`git push *`) cannot let through what
52987
+ * the deny was meant to block. A skipped deny is warned about loudly, because
52988
+ * nothing enforces it, and a widened one is warned about too, because it
52989
+ * blocks more than it says.
52407
52990
  */
52408
52991
  function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
52409
52992
  const result = pattern === "*" ? { target: "*" } : toTarget(action, pattern);
52410
52993
  const rule = `${category}: { "${pattern}": "${decision}" }`;
52411
52994
  if ("skipReason" in result) {
52412
- const prefix = action === "command" && decision === "deny" ? toAntigravityCommandLiteralPrefix(pattern) : void 0;
52995
+ const prefix = decision === "deny" ? toWidenedDenyTarget(action, pattern) : void 0;
52413
52996
  if (prefix !== void 0) {
52414
- logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks every command that starts with \`${prefix}\`, overriding any allow or ask for them.`);
52997
+ const scope = PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``;
52998
+ logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
52999
+ warnHomeOrVariablePath({
53000
+ action,
53001
+ target: prefix,
53002
+ rule,
53003
+ decision,
53004
+ logger,
53005
+ toolLabel
53006
+ });
52415
53007
  return `${action}(${prefix})`;
52416
53008
  }
52417
53009
  const consequence = decision === "deny" ? `This deny is NOT enforced in ${toolLabel}; ${DENY_HINTS[action] ?? "write it in a form Antigravity can match"}` : decision === "ask" ? `${toolLabel} falls back to its default for it, so it runs without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} falls back to its default for it`;
52418
53010
  logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
52419
53011
  return;
52420
53012
  }
52421
- if (result.note !== void 0 && decision !== "deny") logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}.`);
52422
- if (PATH_ACTIONS.has(action) && HOME_OR_VARIABLE_PATH.test(result.target)) {
52423
- const consequence = decision === "deny" ? "this deny may block nothing" : `${toolLabel} may fall back to its default`;
52424
- logger.warn(`${toolLabel} permissions: ${rule}: \`~\` and environment variables may not be expanded in Antigravity path targets, so ${action}(${result.target}) may match nothing and ${consequence}; use an absolute path instead.`);
53013
+ if (result.note !== void 0 && (decision !== "deny" || URL_ACTIONS.has(action))) {
53014
+ const consequence = decision === "deny" ? ", so the deny also overrides any allow or ask there" : "";
53015
+ logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}${consequence}.`);
52425
53016
  }
53017
+ warnHomeOrVariablePath({
53018
+ action,
53019
+ target: result.target,
53020
+ rule,
53021
+ decision,
53022
+ logger,
53023
+ toolLabel
53024
+ });
52426
53025
  return result.target === "*" ? action : `${action}(${result.target})`;
52427
53026
  }
52428
53027
  //#endregion
@@ -52756,7 +53355,8 @@ function pickDocumentedValue({ key, value, documented }) {
52756
53355
  * Deliberate mapping from rulesync canonical permission categories to the
52757
53356
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
52758
53357
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
52759
- * `unsandboxed` which have no canonical equivalent and pass through). `edit` and
53358
+ * `unsandboxed` which have no canonical equivalent and keep their names; their
53359
+ * targets are translated like `read_url` / `command` ones). `edit` and
52760
53360
  * `write` both map to `write_file`, and `webfetch` / `websearch` both map to
52761
53361
  * `read_url`; on import those collapse to the canonical `write` / `webfetch`
52762
53362
  * form (a documented, lossy normalization). This shares the action vocabulary of
@@ -53003,7 +53603,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
53003
53603
  for (const entry of entries) {
53004
53604
  const { toolName, pattern: target } = parsePermissionEntry$1(entry);
53005
53605
  const canonical = toCanonicalToolName$9(toolName);
53006
- const pattern = toolName === "command" ? importAntigravityCommandTarget({
53606
+ const pattern = ANTIGRAVITY_COMMAND_ACTIONS.has(toolName) ? importAntigravityCommandTarget({
53007
53607
  target,
53008
53608
  entry,
53009
53609
  decision: action,
@@ -53025,7 +53625,8 @@ function convertAntigravityCliToRulesyncPermissions(params) {
53025
53625
  * Deliberate mapping from rulesync canonical permission categories to the
53026
53626
  * Antigravity IDE action vocabulary (`read_file`, `write_file`, `read_url`,
53027
53627
  * `command`, `mcp`, plus `execute_url` / `unsandboxed` which have no canonical
53028
- * equivalent and pass through). `edit` and `write` both map to `write_file`, and
53628
+ * equivalent and keep their names; their targets are translated like
53629
+ * `read_url` / `command` ones). `edit` and `write` both map to `write_file`, and
53029
53630
  * `webfetch` / `websearch` both map to `read_url`; on import those collapse to
53030
53631
  * the canonical `write` / `webfetch` form (a documented, lossy normalization).
53031
53632
  */
@@ -53227,7 +53828,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
53227
53828
  for (const entry of entries) {
53228
53829
  const { action: ideAction, pattern: target } = parsePermissionEntry(entry);
53229
53830
  const canonical = toCanonicalCategory$2(ideAction);
53230
- const pattern = ideAction === "command" ? importAntigravityCommandTarget({
53831
+ const pattern = ANTIGRAVITY_COMMAND_ACTIONS.has(ideAction) ? importAntigravityCommandTarget({
53231
53832
  target,
53232
53833
  entry,
53233
53834
  decision: action,
@@ -55157,6 +55758,7 @@ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
55157
55758
  "dialogExpiry",
55158
55759
  "feedbackDrafts",
55159
55760
  "footerLinksRegexes",
55761
+ "forceLoginGatewayUrl",
55160
55762
  "modelPicker",
55161
55763
  "pluginConfigs",
55162
55764
  "prependPlugins",
@@ -55170,6 +55772,39 @@ const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
55170
55772
  "vimInsertModeRemaps"
55171
55773
  ]);
55172
55774
  /**
55775
+ * Nested settings paths under an otherwise project-honored top-level key that
55776
+ * Claude Code reads only from user settings — the nested counterpart of
55777
+ * {@link CLAUDECODE_USER_SCOPE_ONLY_KEYS}. `worktree` is `Any file`, but
55778
+ * `worktree.location` (where Desktop SSH sessions create worktrees on the
55779
+ * remote machine) is documented with a `User` scope, so it is dropped from a
55780
+ * project `.claude/settings.json` and written only under `--global`, while its
55781
+ * siblings are kept.
55782
+ *
55783
+ * @see https://code.claude.com/docs/en/settings-reference#worktree-location
55784
+ */
55785
+ const CLAUDECODE_USER_SCOPE_ONLY_NESTED_PATHS = [["worktree", "location"]];
55786
+ /**
55787
+ * Drops {@link CLAUDECODE_USER_SCOPE_ONLY_NESTED_PATHS} from the top-level
55788
+ * passthrough at project scope, in place, warning once per dropped path. The
55789
+ * authored subtree is copied before the deletion so the caller's config is not
55790
+ * mutated, and a parent the removal emptied is dropped with it.
55791
+ */
55792
+ function stripUserScopeOnlyNestedPaths({ overrides, global, relativeFilePath, logger }) {
55793
+ if (global) return;
55794
+ for (const path of CLAUDECODE_USER_SCOPE_ONLY_NESTED_PATHS) {
55795
+ const [topLevelKey] = path;
55796
+ if (topLevelKey === void 0 || !isRecord$1(overrides[topLevelKey])) continue;
55797
+ const copy = { [topLevelKey]: structuredClone(overrides[topLevelKey]) };
55798
+ if (!deleteSandboxPath({
55799
+ target: copy,
55800
+ path
55801
+ })) continue;
55802
+ if (copy[topLevelKey] === void 0) delete overrides[topLevelKey];
55803
+ else overrides[topLevelKey] = copy[topLevelKey];
55804
+ 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.`);
55805
+ }
55806
+ }
55807
+ /**
55173
55808
  * Top-level settings keys neither file rulesync writes can honor, with the file
55174
55809
  * that does. `Managed` keys are read only from the settings file an organization
55175
55810
  * deploys, and `Global config` keys only from `~/.claude.json` — rulesync writes
@@ -55207,7 +55842,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
55207
55842
  disableMobileSimulatorTools: "managed settings",
55208
55843
  disableSideloadFlags: "managed settings",
55209
55844
  externalEditorContext: "~/.claude.json",
55210
- forceLoginGatewayUrl: "managed settings",
55211
55845
  forceRemoteSettingsRefresh: "managed settings",
55212
55846
  gatewayInternalNetworks: "managed settings",
55213
55847
  leftArrowOpensAgents: "~/.claude.json",
@@ -55366,8 +56000,9 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
55366
56000
  /**
55367
56001
  * `env` variables a project or local settings file cannot set, because a
55368
56002
  * checked-out repository should not control them: where Claude Code stores its
55369
- * own files, what session content it exports, where telemetry goes, and how it
55370
- * starts or syncs. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
56003
+ * own files, what session content it exports, where telemetry goes, how it
56004
+ * starts or syncs, the timers on an unanswered dialog, and whether it processes
56005
+ * attachments or expands `@` file mentions. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
55371
56006
  *
55372
56007
  * Only the names the settings reference lists are covered; its directory and
55373
56008
  * Windows groups are introduced with "such as", so an unlisted member of those
@@ -55378,6 +56013,10 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
55378
56013
  */
55379
56014
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
55380
56015
  "BETA_TRACING_ENDPOINT",
56016
+ "CLAUDE_AFK_COUNTDOWN_MS",
56017
+ "CLAUDE_AFK_TIMEOUT_MS",
56018
+ "CLAUDE_CODE_DISABLE_ATTACHMENTS",
56019
+ "CLAUDE_CODE_DISABLE_FILE_MENTIONS",
55381
56020
  "CLAUDE_CODE_ENABLE_TELEMETRY",
55382
56021
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
55383
56022
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
@@ -55386,6 +56025,7 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
55386
56025
  "CLAUDE_CODE_SYNC_PLUGINS",
55387
56026
  "CLAUDE_CODE_SYNC_SKILLS",
55388
56027
  "CLAUDE_CODE_TMPDIR",
56028
+ "CLAUDE_CODE_USER_DIALOG_TIMEOUT_MS",
55389
56029
  "CLAUDE_CONFIG_DIR",
55390
56030
  "ENABLE_BETA_TRACING_DETAILED",
55391
56031
  "ENABLE_ENHANCED_TELEMETRY_BETA",
@@ -55431,6 +56071,13 @@ const CLAUDECODE_ENV_CONTENT_OFF_VALUES = /* @__PURE__ */ new Set([
55431
56071
  * writing a value Claude Code ignores is the old behavior, while dropping an
55432
56072
  * opt-out it honors would remove a privacy setting. `OTEL_LOG_ASSISTANT_RESPONSES`
55433
56073
  * has no such exception and sits in the plain name table above.
56074
+ *
56075
+ * `CLAUDE_CODE_ENABLE_CFC` follows the same shape for a different reason: a
56076
+ * project cannot turn Claude in Chrome on, since a checked-out repository must
56077
+ * not be able to connect Claude to the user's browser, so only an off value is
56078
+ * kept at project scope.
56079
+ *
56080
+ * @see https://code.claude.com/docs/en/chrome#project-settings-cant-turn-on-chrome
55434
56081
  */
55435
56082
  const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
55436
56083
  OTEL_LOGS_EXPORTER: CLAUDECODE_ENV_EXPORTER_OFF_VALUES,
@@ -55438,7 +56085,8 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
55438
56085
  OTEL_TRACES_EXPORTER: CLAUDECODE_ENV_EXPORTER_OFF_VALUES,
55439
56086
  OTEL_LOG_USER_PROMPTS: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
55440
56087
  OTEL_LOG_TOOL_CONTENT: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
55441
- OTEL_LOG_TOOL_DETAILS: CLAUDECODE_ENV_CONTENT_OFF_VALUES
56088
+ OTEL_LOG_TOOL_DETAILS: CLAUDECODE_ENV_CONTENT_OFF_VALUES,
56089
+ CLAUDE_CODE_ENABLE_CFC: CLAUDECODE_ENV_CONTENT_OFF_VALUES
55442
56090
  };
55443
56091
  function isProjectIgnoredEnvVariable(name, value) {
55444
56092
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_")) return true;
@@ -55475,7 +56123,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
55475
56123
  kept[name] = value;
55476
56124
  }
55477
56125
  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.`);
55478
- 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.`);
56126
+ 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.`);
55479
56127
  return kept;
55480
56128
  }
55481
56129
  /**
@@ -55706,6 +56354,12 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
55706
56354
  relativeFilePath: paths.relativeFilePath,
55707
56355
  logger
55708
56356
  });
56357
+ stripUserScopeOnlyNestedPaths({
56358
+ overrides: overrideTopLevel,
56359
+ global,
56360
+ relativeFilePath: paths.relativeFilePath,
56361
+ logger
56362
+ });
55709
56363
  const { filtered: scopedTopLevel, trustAffecting: trustAffectingTopLevel } = stripUnhonoredTopLevelKeys({
55710
56364
  overrides: overrideTopLevel,
55711
56365
  global,
@@ -56123,7 +56777,7 @@ const CODEBUDDY_RULE_LISTS = [
56123
56777
  "ask",
56124
56778
  "deny"
56125
56779
  ];
56126
- const CATCH_ALL_PATTERN$11 = "*";
56780
+ const CATCH_ALL_PATTERN$12 = "*";
56127
56781
  const MCP_CATEGORY = "mcp";
56128
56782
  const MCP_RULE_PREFIX = "mcp__";
56129
56783
  const ALL_MCP_RULE = "mcp__*";
@@ -56196,7 +56850,7 @@ function categoryOfRule$2(rule) {
56196
56850
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
56197
56851
  */
56198
56852
  function buildCodebuddyRule({ category, pattern, action, logger }) {
56199
- const catchAll = pattern === CATCH_ALL_PATTERN$11;
56853
+ const catchAll = pattern === CATCH_ALL_PATTERN$12;
56200
56854
  if (category === MCP_CATEGORY) {
56201
56855
  if (catchAll && action === "allow") {
56202
56856
  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').`);
@@ -56205,7 +56859,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
56205
56859
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
56206
56860
  }
56207
56861
  if (category === "*") {
56208
- if (catchAll) return CATCH_ALL_PATTERN$11;
56862
+ if (catchAll) return CATCH_ALL_PATTERN$12;
56209
56863
  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').`);
56210
56864
  return null;
56211
56865
  }
@@ -56223,7 +56877,7 @@ function parseCodebuddyRule(rule) {
56223
56877
  const { specifier } = splitCodebuddyRule(rule);
56224
56878
  if (specifier === void 0) return {
56225
56879
  category,
56226
- pattern: CATCH_ALL_PATTERN$11
56880
+ pattern: CATCH_ALL_PATTERN$12
56227
56881
  };
56228
56882
  const trimmed = specifier.trim();
56229
56883
  return trimmed === "" ? null : {
@@ -58474,7 +59128,7 @@ function mapBashActionToDecision(action) {
58474
59128
  //#region src/features/permissions/commandcode-permissions.ts
58475
59129
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
58476
59130
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
58477
- const CATCH_ALL_PATTERN$10 = "*";
59131
+ const CATCH_ALL_PATTERN$11 = "*";
58478
59132
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
58479
59133
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
58480
59134
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -58499,14 +59153,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
58499
59153
  web_fetch: "webfetch",
58500
59154
  web_search: "websearch"
58501
59155
  };
58502
- const ACTION_RANK$4 = {
59156
+ const ACTION_RANK$5 = {
58503
59157
  allow: 0,
58504
59158
  ask: 1,
58505
59159
  deny: 2
58506
59160
  };
58507
59161
  /** Whether `action` outranks `existing` (deny > ask > allow). */
58508
59162
  function isStricterAction({ action, existing }) {
58509
- return existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing];
59163
+ return existing === void 0 || ACTION_RANK$5[action] > ACTION_RANK$5[existing];
58510
59164
  }
58511
59165
  /**
58512
59166
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -58529,10 +59183,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
58529
59183
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
58530
59184
  */
58531
59185
  function buildCommandcodeRule(category, pattern) {
58532
- const catchAll = pattern === CATCH_ALL_PATTERN$10 || pattern === "";
59186
+ const catchAll = pattern === CATCH_ALL_PATTERN$11 || pattern === "";
58533
59187
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
58534
59188
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
58535
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$10 : null;
59189
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$11 : null;
58536
59190
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
58537
59191
  if (tool === void 0) return null;
58538
59192
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -58568,7 +59222,7 @@ function splitCommandcodeRule(rule) {
58568
59222
  }
58569
59223
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
58570
59224
  function isCatchAllSpecifier(inner) {
58571
- return inner === "" || inner === CATCH_ALL_PATTERN$10;
59225
+ return inner === "" || inner === CATCH_ALL_PATTERN$11;
58572
59226
  }
58573
59227
  /**
58574
59228
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -58602,34 +59256,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
58602
59256
  if (server.length === 0) return null;
58603
59257
  const everyServer = {
58604
59258
  category: "mcp",
58605
- pattern: CATCH_ALL_PATTERN$10,
59259
+ pattern: CATCH_ALL_PATTERN$11,
58606
59260
  notInAllow: true
58607
59261
  };
58608
59262
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
58609
- if (server === CATCH_ALL_PATTERN$10) return everyServer;
58610
- if (server.includes(CATCH_ALL_PATTERN$10)) return null;
59263
+ if (server === CATCH_ALL_PATTERN$11) return everyServer;
59264
+ if (server.includes(CATCH_ALL_PATTERN$11)) return null;
58611
59265
  return {
58612
- category: rest === "" || rest === CATCH_ALL_PATTERN$10 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
58613
- pattern: CATCH_ALL_PATTERN$10,
59266
+ category: rest === "" || rest === CATCH_ALL_PATTERN$11 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
59267
+ pattern: CATCH_ALL_PATTERN$11,
58614
59268
  notInAllow: false
58615
59269
  };
58616
59270
  }
58617
59271
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
58618
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$10);
59272
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$11);
58619
59273
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
58620
59274
  category,
58621
59275
  pattern: inner,
58622
59276
  notInAllow: true
58623
59277
  } : null;
58624
- if (remainder === CATCH_ALL_PATTERN$10) return everyServer;
58625
- if (rest === CATCH_ALL_PATTERN$10 && !server.includes(CATCH_ALL_PATTERN$10)) return {
59278
+ if (remainder === CATCH_ALL_PATTERN$11) return everyServer;
59279
+ if (rest === CATCH_ALL_PATTERN$11 && !server.includes(CATCH_ALL_PATTERN$11)) return {
58626
59280
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
58627
- pattern: CATCH_ALL_PATTERN$10,
59281
+ pattern: CATCH_ALL_PATTERN$11,
58628
59282
  notInAllow: true
58629
59283
  };
58630
59284
  return namesTool ? {
58631
59285
  category,
58632
- pattern: CATCH_ALL_PATTERN$10,
59286
+ pattern: CATCH_ALL_PATTERN$11,
58633
59287
  notInAllow: false
58634
59288
  } : null;
58635
59289
  }
@@ -58653,14 +59307,14 @@ function friendlyToolRule({ category, inner }) {
58653
59307
  if (category === "bash") {
58654
59308
  if (isCatchAllSpecifier(inner)) return {
58655
59309
  category,
58656
- pattern: CATCH_ALL_PATTERN$10,
59310
+ pattern: CATCH_ALL_PATTERN$11,
58657
59311
  notInAllow: false
58658
59312
  };
58659
59313
  const normalized = inner.trim().replace(/\s+/g, " ");
58660
59314
  if (normalized === "" || normalized === ":*") return null;
58661
59315
  return isCatchAllSpecifier(normalized) ? {
58662
59316
  category,
58663
- pattern: CATCH_ALL_PATTERN$10,
59317
+ pattern: CATCH_ALL_PATTERN$11,
58664
59318
  notInAllow: true
58665
59319
  } : {
58666
59320
  category,
@@ -58670,7 +59324,7 @@ function friendlyToolRule({ category, inner }) {
58670
59324
  }
58671
59325
  if (isCatchAllSpecifier(inner)) return {
58672
59326
  category,
58673
- pattern: CATCH_ALL_PATTERN$10,
59327
+ pattern: CATCH_ALL_PATTERN$11,
58674
59328
  notInAllow: false
58675
59329
  };
58676
59330
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -58689,9 +59343,9 @@ function friendlyToolRule({ category, inner }) {
58689
59343
  */
58690
59344
  function parseCommandcodeRule(rule) {
58691
59345
  const { tool, inner } = splitCommandcodeRule(rule);
58692
- if (tool === CATCH_ALL_PATTERN$10) return isCatchAllSpecifier(inner) ? {
59346
+ if (tool === CATCH_ALL_PATTERN$11) return isCatchAllSpecifier(inner) ? {
58693
59347
  category: "*",
58694
- pattern: CATCH_ALL_PATTERN$10,
59348
+ pattern: CATCH_ALL_PATTERN$11,
58695
59349
  notInAllow: true
58696
59350
  } : null;
58697
59351
  const lowered = tool.toLowerCase();
@@ -58761,7 +59415,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
58761
59415
  }).filter((rule) => {
58762
59416
  const parsed = parseCommandcodeRule(rule);
58763
59417
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
58764
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$10})`);
59418
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$11})`);
58765
59419
  if (key !== "allow" && isStricterAction({
58766
59420
  action: key,
58767
59421
  existing: replacement
@@ -58818,7 +59472,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
58818
59472
  const ranked = /* @__PURE__ */ new Map();
58819
59473
  const managedCategories = /* @__PURE__ */ new Set();
58820
59474
  for (const [category, rules] of Object.entries(permission)) {
58821
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$10);
59475
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$11);
58822
59476
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
58823
59477
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
58824
59478
  for (const [pattern, action] of Object.entries(rules)) {
@@ -59089,7 +59743,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
59089
59743
  //#endregion
59090
59744
  //#region src/features/permissions/continue-permissions.ts
59091
59745
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
59092
- const CATCH_ALL_PATTERN$9 = "*";
59746
+ const CATCH_ALL_PATTERN$10 = "*";
59093
59747
  /**
59094
59748
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
59095
59749
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -59097,7 +59751,7 @@ const CATCH_ALL_PATTERN$9 = "*";
59097
59751
  * the bare entry instead of being skipped as a pattern-specific rule.
59098
59752
  */
59099
59753
  function normalizeCatchAllPattern$1(pattern) {
59100
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
59754
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
59101
59755
  }
59102
59756
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
59103
59757
  bash: "Bash",
@@ -59134,7 +59788,7 @@ function toCanonicalToolName$6(continueName) {
59134
59788
  * `Tool(pattern)`.
59135
59789
  */
59136
59790
  function buildContinuePermissionEntry(toolName, pattern) {
59137
- return pattern === CATCH_ALL_PATTERN$9 ? toolName : `${toolName}(${pattern})`;
59791
+ return pattern === CATCH_ALL_PATTERN$10 ? toolName : `${toolName}(${pattern})`;
59138
59792
  }
59139
59793
  /**
59140
59794
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -59148,7 +59802,7 @@ function parseContinuePermissionEntry(entry) {
59148
59802
  if (toolName === "") return;
59149
59803
  return {
59150
59804
  toolName,
59151
- pattern: pattern === "" ? CATCH_ALL_PATTERN$9 : pattern
59805
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$10 : pattern
59152
59806
  };
59153
59807
  }
59154
59808
  /**
@@ -59322,7 +59976,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
59322
59976
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
59323
59977
  continue;
59324
59978
  }
59325
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$9) {
59979
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$10) {
59326
59980
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
59327
59981
  continue;
59328
59982
  }
@@ -59690,14 +60344,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
59690
60344
  };
59691
60345
  //#endregion
59692
60346
  //#region src/features/permissions/crush-permissions.ts
59693
- const CATCH_ALL_PATTERN$8 = "*";
60347
+ const CATCH_ALL_PATTERN$9 = "*";
59694
60348
  /**
59695
60349
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
59696
60350
  * argument patterns at all, so it is the only pattern a rule can carry and
59697
60351
  * still be written.
59698
60352
  */
59699
60353
  function normalizeCatchAllPattern(pattern) {
59700
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
60354
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
59701
60355
  }
59702
60356
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
59703
60357
  bash: "bash",
@@ -59896,7 +60550,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
59896
60550
  for (const [rawPattern, action] of Object.entries(rules)) {
59897
60551
  if (isPrototypePollutionKey(rawPattern)) continue;
59898
60552
  const pattern = normalizeCatchAllPattern(rawPattern);
59899
- if (pattern === CATCH_ALL_PATTERN$8) {
60553
+ if (pattern === CATCH_ALL_PATTERN$9) {
59900
60554
  catchAllAction = action;
59901
60555
  continue;
59902
60556
  }
@@ -59969,11 +60623,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
59969
60623
  };
59970
60624
  for (const entry of disabled) {
59971
60625
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
59972
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] = "deny";
60626
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] = "deny";
59973
60627
  }
59974
60628
  for (const entry of allowed) {
59975
60629
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
59976
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] ??= "allow";
60630
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] ??= "allow";
59977
60631
  }
59978
60632
  return { permission };
59979
60633
  }
@@ -61838,7 +62492,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
61838
62492
  //#region src/features/permissions/goose-permissions.ts
61839
62493
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
61840
62494
  const GOOSE_USER_KEY = "user";
61841
- const CATCH_ALL_PATTERN$7 = "*";
62495
+ const CATCH_ALL_PATTERN$8 = "*";
61842
62496
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
61843
62497
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
61844
62498
  shell: ["bash"],
@@ -61850,7 +62504,7 @@ const ACTION_TO_GOOSE_LIST = {
61850
62504
  ask: "ask_before",
61851
62505
  deny: "never_allow"
61852
62506
  };
61853
- const ACTION_RANK$3 = {
62507
+ const ACTION_RANK$4 = {
61854
62508
  allow: 0,
61855
62509
  ask: 1,
61856
62510
  deny: 2
@@ -62002,14 +62656,14 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
62002
62656
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
62003
62657
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
62004
62658
  for (const [pattern, action] of Object.entries(rules)) {
62005
- if (pattern !== CATCH_ALL_PATTERN$7) {
62659
+ if (pattern !== CATCH_ALL_PATTERN$8) {
62006
62660
  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.`);
62007
62661
  continue;
62008
62662
  }
62009
62663
  const previous = assigned.get(toolName);
62010
62664
  if (previous === action) continue;
62011
62665
  if (previous !== void 0) {
62012
- const stricter = ACTION_RANK$3[action] > ACTION_RANK$3[previous] ? action : previous;
62666
+ const stricter = ACTION_RANK$4[action] > ACTION_RANK$4[previous] ? action : previous;
62013
62667
  logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
62014
62668
  if (stricter === previous) continue;
62015
62669
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
@@ -62033,7 +62687,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
62033
62687
  const action = GOOSE_LIST_TO_ACTION[key];
62034
62688
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
62035
62689
  permission[category] ??= {};
62036
- permission[category][CATCH_ALL_PATTERN$7] = action;
62690
+ permission[category][CATCH_ALL_PATTERN$8] = action;
62037
62691
  }
62038
62692
  }
62039
62693
  return { permission };
@@ -62044,7 +62698,7 @@ const GROKCLI_UI_KEY = "ui";
62044
62698
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
62045
62699
  const GROKCLI_PERMISSION_KEY = "permission";
62046
62700
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
62047
- const CATCH_ALL_PATTERN$6 = "*";
62701
+ const CATCH_ALL_PATTERN$7 = "*";
62048
62702
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
62049
62703
  const CATEGORY_TO_GROK_TOOL = {
62050
62704
  bash: "Bash",
@@ -62069,6 +62723,8 @@ const GROK_TOOL_TO_CATEGORY = {
62069
62723
  ...Object.fromEntries(["SendSubagentMessage", "SendAgentMessage"].map((tool) => [tool, "agent"]))
62070
62724
  };
62071
62725
  const GROK_MCP_TOOL = "MCPTool";
62726
+ const GROK_ALL_TOOLS_ENTRY = "*";
62727
+ const GROK_VERBOSE_ANY_TOOL = "any";
62072
62728
  const GROK_TOOL_TO_CATEGORY_LOWER = {
62073
62729
  ...Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category])),
62074
62730
  agent_message: "agent"
@@ -62083,22 +62739,26 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
62083
62739
  * Scoped MCP categories (`mcp__<remainder>`) fold their address into the
62084
62740
  * parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
62085
62741
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
62086
- * concrete pattern emits `Tool(pattern)`.
62742
+ * concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
62743
+ * Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
62744
+ * under it has no Grok spelling and returns `null`.
62087
62745
  */
62088
62746
  function buildGrokEntry(category, pattern) {
62747
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$7 ? GROK_ALL_TOOLS_ENTRY : null;
62089
62748
  if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
62090
62749
  const remainder = category.slice(5);
62091
62750
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
62092
62751
  }
62093
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
62752
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
62094
62753
  const tool = CATEGORY_TO_GROK_TOOL[category];
62095
62754
  if (tool === void 0) return null;
62096
- return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? tool : `${tool}(${pattern})`;
62755
+ return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? tool : `${tool}(${pattern})`;
62097
62756
  }
62098
62757
  /**
62099
62758
  * Parse a Grok Claude-style permission entry back into a canonical category +
62100
- * pattern. Entries without parentheses are treated as catch-all (`*`). Returns
62101
- * `null` for tool prefixes with no canonical equivalent (e.g. `any`).
62759
+ * pattern. Entries without parentheses are treated as catch-all (`*`), and the
62760
+ * bare `*` rule becomes the all-tools `*` category. Returns `null` for tool
62761
+ * prefixes with no canonical equivalent.
62102
62762
  */
62103
62763
  function parseGrokEntry(entry) {
62104
62764
  const trimmed = entry.trim();
@@ -62112,19 +62772,23 @@ function parseGrokEntry(entry) {
62112
62772
  tool = trimmed.slice(0, parenIndex);
62113
62773
  inner = trimmed.slice(parenIndex + 1, -1).trim();
62114
62774
  }
62775
+ if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
62776
+ category: "*",
62777
+ pattern: CATCH_ALL_PATTERN$7
62778
+ };
62115
62779
  if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$3)) return parseClaudeStyleMcpEntry(tool);
62116
62780
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
62117
62781
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
62118
- pattern: CATCH_ALL_PATTERN$6
62782
+ pattern: CATCH_ALL_PATTERN$7
62119
62783
  } : {
62120
62784
  category: "mcp",
62121
- pattern: CATCH_ALL_PATTERN$6
62785
+ pattern: CATCH_ALL_PATTERN$7
62122
62786
  };
62123
62787
  const category = GROK_TOOL_TO_CATEGORY[tool];
62124
62788
  if (category === void 0) return null;
62125
62789
  return {
62126
62790
  category,
62127
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$6
62791
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$7
62128
62792
  };
62129
62793
  }
62130
62794
  /**
@@ -62141,14 +62805,14 @@ function parseGrokEntry(entry) {
62141
62805
  function parseClaudeStyleMcpEntry(entry) {
62142
62806
  const rest = entry.slice(5);
62143
62807
  if (rest.length === 0) return null;
62144
- if (rest === CATCH_ALL_PATTERN$6) return {
62808
+ if (rest === CATCH_ALL_PATTERN$7) return {
62145
62809
  category: "mcp",
62146
- pattern: CATCH_ALL_PATTERN$6
62810
+ pattern: CATCH_ALL_PATTERN$7
62147
62811
  };
62148
62812
  const address = rest.includes("__") ? rest : `${rest}__*`;
62149
62813
  return {
62150
62814
  category: `${MCP_CANONICAL_PREFIX$3}${address}`,
62151
- pattern: CATCH_ALL_PATTERN$6
62815
+ pattern: CATCH_ALL_PATTERN$7
62152
62816
  };
62153
62817
  }
62154
62818
  /**
@@ -62169,15 +62833,20 @@ function parseGrokRule(rule) {
62169
62833
  if (typeof tool !== "string") return null;
62170
62834
  if (pattern !== void 0 && typeof pattern !== "string") return null;
62171
62835
  const trimmedPattern = pattern?.trim() ?? "";
62172
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$6;
62836
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$7;
62173
62837
  const lowerTool = tool.trim().toLowerCase();
62174
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$6 ? {
62838
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
62839
+ category: "*",
62840
+ pattern: CATCH_ALL_PATTERN$7,
62841
+ action
62842
+ } : null;
62843
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
62175
62844
  category: "mcp",
62176
- pattern: CATCH_ALL_PATTERN$6,
62845
+ pattern: CATCH_ALL_PATTERN$7,
62177
62846
  action
62178
62847
  } : {
62179
62848
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
62180
- pattern: CATCH_ALL_PATTERN$6,
62849
+ pattern: CATCH_ALL_PATTERN$7,
62181
62850
  action
62182
62851
  };
62183
62852
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -62207,7 +62876,10 @@ function parseGrokRule(rule) {
62207
62876
  * subagent rather than its launch, and whose pattern is a subagent id
62208
62877
  * rather than a subagent type); `mcp__*` maps to `MCPTool(...)` (a scoped
62209
62878
  * MCP category folds its address into the parentheses, so a non-`*`
62210
- * argument pattern on it is not represented). The one category with no Grok tool (`notebookedit`) is
62879
+ * argument pattern on it is not represented); the all-tools `*` category
62880
+ * maps to Grok's bare `*` rule (which matches every tool) for its
62881
+ * catch-all pattern, while a narrower `*` pattern is skipped with a
62882
+ * warning. The one category with no Grok tool (`notebookedit`) is
62211
62883
  * skipped (with a warning when it carries a `deny` rule, to surface the
62212
62884
  * gap). When two canonical rules collapse onto the same Grok
62213
62885
  * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
@@ -62218,7 +62890,9 @@ function parseGrokRule(rule) {
62218
62890
  * verbose `[[permission.rules]]` tables
62219
62891
  * (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
62220
62892
  * field is matched case-insensitively against the same tool table, plus
62221
- * the documented `mcp` alias for the compact form's `MCPTool`. Rules
62893
+ * the documented `mcp` alias for the compact form's `MCPTool`. A bare `*`
62894
+ * entry and a tool-wide verbose `any` rule both import as the all-tools
62895
+ * `*` category. Rules
62222
62896
  * from the two forms are merged with the same `deny > ask > allow`
62223
62897
  * precedence. When neither form carries a rule (older configs), the coarse
62224
62898
  * `[ui] permission_mode` is used as a fallback. Generate always emits the
@@ -62243,7 +62917,7 @@ function parseGrokRule(rule) {
62243
62917
  * tools it models and (in global scope) the `[ui] permission_mode` value, while
62244
62918
  * every other key
62245
62919
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
62246
- * entries for tool prefixes rulesync cannot model (e.g. `any`) are preserved.
62920
+ * entries for tool prefixes rulesync cannot model (e.g. a pattern-only entry such as `src/**`) are preserved.
62247
62921
  * The file is never deleted.
62248
62922
  */
62249
62923
  var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
@@ -62325,7 +62999,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
62325
62999
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
62326
63000
  }
62327
63001
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
62328
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$6]: legacyModeAction(parsed) } } };
63002
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$7]: legacyModeAction(parsed) } } };
62329
63003
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
62330
63004
  }
62331
63005
  validate() {
@@ -62345,14 +63019,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
62345
63019
  });
62346
63020
  }
62347
63021
  };
62348
- const ACTION_RANK$2 = {
63022
+ const ACTION_RANK$3 = {
62349
63023
  allow: 0,
62350
63024
  ask: 1,
62351
63025
  deny: 2
62352
63026
  };
62353
63027
  /**
62354
63028
  * Collect user-authored entries from an existing `[permission]` array whose
62355
- * tool prefix rulesync cannot model (e.g. `any`). Such entries are
63029
+ * tool prefix rulesync cannot model (e.g. a pattern-only entry such as `src/**`). Such entries are
62356
63030
  * preserved verbatim so replacing the arrays does not silently drop them
62357
63031
  * (mirrors the Cursor adapter's preservation of unmanaged types).
62358
63032
  */
@@ -62371,13 +63045,26 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
62371
63045
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
62372
63046
  const entry = buildGrokEntry(category, pattern);
62373
63047
  if (entry === null) {
63048
+ if (category === "*") {
63049
+ 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').`);
63050
+ continue;
63051
+ }
62374
63052
  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.`);
62375
63053
  continue;
62376
63054
  }
62377
63055
  const existing = ranked.get(entry);
62378
63056
  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).`);
62379
- if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) ranked.set(entry, action);
63057
+ if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) ranked.set(entry, action);
62380
63058
  }
63059
+ warnRemovedAllToolsRestrictions({
63060
+ existingPermission,
63061
+ ranked,
63062
+ logger
63063
+ });
63064
+ warnEntriesShadowedByAllTools({
63065
+ ranked,
63066
+ logger
63067
+ });
62381
63068
  const allow = unmanagedEntries(existingPermission, "allow");
62382
63069
  const deny = unmanagedEntries(existingPermission, "deny");
62383
63070
  const ask = unmanagedEntries(existingPermission, "ask");
@@ -62391,6 +63078,38 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
62391
63078
  };
62392
63079
  }
62393
63080
  /**
63081
+ * Warn when generate drops an existing bare `*` `deny`/`ask` entry without
63082
+ * writing an equally strict one back. Such an entry is Rulesync-owned (it maps
63083
+ * to the all-tools `*` category), so a config without a matching canonical
63084
+ * rule removes it — which silently lifts a restriction on every tool.
63085
+ */
63086
+ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger }) {
63087
+ if (!logger) return;
63088
+ const generated = ranked.get(GROK_ALL_TOOLS_ENTRY);
63089
+ for (const action of ["deny", "ask"]) {
63090
+ const entries = existingPermission[action];
63091
+ if (!isStringArray$2(entries)) continue;
63092
+ if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63093
+ if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
63094
+ 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.`);
63095
+ }
63096
+ }
63097
+ /**
63098
+ * Warn about entries that a bare `*` `deny`/`ask` makes ineffective: Grok
63099
+ * evaluates by severity (`deny > ask > allow`), so a specific `allow` (and,
63100
+ * under a `*` deny, a specific `ask`) never takes effect next to it. Shell
63101
+ * rules are already folded by `honorAllToolsOnBash`, so this covers the other
63102
+ * tools.
63103
+ */
63104
+ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63105
+ if (!logger) return;
63106
+ const allTools = ranked.get(GROK_ALL_TOOLS_ENTRY);
63107
+ if (allTools === void 0 || allTools === "allow") return;
63108
+ const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$3[action] < ACTION_RANK$3[allTools]).map(([entry]) => entry).toSorted();
63109
+ if (shadowed.length === 0) return;
63110
+ 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(", ")}.`);
63111
+ }
63112
+ /**
62394
63113
  * Parse Grok's `[permission]` section back into a canonical permission map,
62395
63114
  * reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
62396
63115
  * Claude-style entries and the verbose `[[permission.rules]]` tables
@@ -62409,7 +63128,7 @@ function parseGrokPermissionArrays(permission) {
62409
63128
  const record = ({ category, pattern, action }) => {
62410
63129
  const bucket = result[category] ??= {};
62411
63130
  const existing = bucket[pattern];
62412
- if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) bucket[pattern] = action;
63131
+ if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) bucket[pattern] = action;
62413
63132
  };
62414
63133
  const applyEntries = (entries, action) => {
62415
63134
  for (const entry of entries ?? []) {
@@ -62813,7 +63532,7 @@ const JUNIE_GROUP_TO_CANONICAL = {
62813
63532
  mcpTools: "mcp",
62814
63533
  readOutsideProject: "read"
62815
63534
  };
62816
- function isPermissionAction$1(value) {
63535
+ function isPermissionAction$2(value) {
62817
63536
  return PermissionActionSchema.safeParse(value).success;
62818
63537
  }
62819
63538
  /**
@@ -62999,7 +63718,7 @@ function convertJunieToRulesyncPermissions({ allowlist }) {
62999
63718
  for (const rule of Array.isArray(ruleSet.rules) ? ruleSet.rules : []) {
63000
63719
  if (!rule || typeof rule !== "object") continue;
63001
63720
  const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
63002
- if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
63721
+ if (pattern === void 0 || !isPermissionAction$2(rule.action)) continue;
63003
63722
  (permission[category] ??= {})[pattern] = rule.action;
63004
63723
  }
63005
63724
  }
@@ -63958,6 +64677,367 @@ function rulesFromArrays(settings, allowKey, denyKey) {
63958
64677
  return rules;
63959
64678
  }
63960
64679
  //#endregion
64680
+ //#region src/features/permissions/kiro-capability-permissions.ts
64681
+ const CATCH_ALL_PATTERN$6 = "*";
64682
+ const CANONICAL_TO_KIRO_CAPABILITY = {
64683
+ bash: "shell",
64684
+ read: "fs_read",
64685
+ edit: "fs_write",
64686
+ write: "fs_write",
64687
+ webfetch: "web_fetch",
64688
+ websearch: "web_search"
64689
+ };
64690
+ const KIRO_CAPABILITY_TO_CANONICAL = {
64691
+ shell: "bash",
64692
+ fs_read: "read",
64693
+ fs_write: "write",
64694
+ web_fetch: "webfetch",
64695
+ web_search: "websearch"
64696
+ };
64697
+ const MANAGED_CAPABILITIES = new Set(Object.keys(KIRO_CAPABILITY_TO_CANONICAL));
64698
+ const ACTION_RANK$2 = {
64699
+ allow: 0,
64700
+ ask: 1,
64701
+ deny: 2
64702
+ };
64703
+ function isPermissionAction$1(value) {
64704
+ return value === "allow" || value === "ask" || value === "deny";
64705
+ }
64706
+ /**
64707
+ * Whether an existing `permissions.yaml` rule can be read back into the
64708
+ * canonical model: a rule for a capability rulesync maps, with no `exclude`
64709
+ * list (the canonical model has no exclusion). Used on import only.
64710
+ */
64711
+ function isImportableRule(rule) {
64712
+ return isRecord$1(rule) && typeof rule.capability === "string" && MANAGED_CAPABILITIES.has(rule.capability) && rule.exclude === void 0;
64713
+ }
64714
+ /**
64715
+ * Key identifying one (capability, pattern) pair. The catch-all `*` stands for
64716
+ * a match-less rule.
64717
+ */
64718
+ function rulePatternKey(capability, pattern) {
64719
+ return `${capability}\u0000${pattern}`;
64720
+ }
64721
+ /**
64722
+ * Classify one existing (capability, pattern, effect) entry against the
64723
+ * generated rules: an exact duplicate (same effect) is replaced by the
64724
+ * generated rule; a different effect is a conflict and is kept; anything
64725
+ * rulesync does not emit is unrelated and kept.
64726
+ */
64727
+ function classifyExistingEntry({ emitted, capability, pattern, effect }) {
64728
+ const effects = emitted.get(rulePatternKey(capability, pattern));
64729
+ if (effects === void 0) return "unrelated";
64730
+ return isPermissionAction$1(effect) && effects.has(effect) ? "duplicate" : "conflict";
64731
+ }
64732
+ /**
64733
+ * Merge one existing rule against the generated entries (see
64734
+ * {@link mergeKiroCapabilityRules}). Returns the rule to keep (possibly with a
64735
+ * narrowed `match`), or `undefined` when every entry is an exact duplicate, and
64736
+ * how many entries rulesync does not generate.
64737
+ */
64738
+ function mergeExistingRule({ rule, emitted, logger }) {
64739
+ 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 {
64740
+ rule,
64741
+ unrelatedEntries: 0
64742
+ };
64743
+ const capability = rule.capability;
64744
+ const isCatchAll = rule.match === void 0;
64745
+ const patterns = isStringArray$2(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
64746
+ if (patterns.length === 0) return {
64747
+ rule,
64748
+ unrelatedEntries: 0
64749
+ };
64750
+ const remaining = [];
64751
+ let unrelatedEntries = 0;
64752
+ for (const pattern of patterns) {
64753
+ const outcome = classifyExistingEntry({
64754
+ emitted,
64755
+ capability,
64756
+ pattern,
64757
+ effect: rule.effect
64758
+ });
64759
+ if (outcome === "duplicate") continue;
64760
+ remaining.push(pattern);
64761
+ if (outcome === "unrelated") {
64762
+ unrelatedEntries++;
64763
+ continue;
64764
+ }
64765
+ 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).`);
64766
+ }
64767
+ if (remaining.length === 0) return {
64768
+ rule: void 0,
64769
+ unrelatedEntries
64770
+ };
64771
+ if (remaining.length === patterns.length || isCatchAll) return {
64772
+ rule,
64773
+ unrelatedEntries
64774
+ };
64775
+ return {
64776
+ rule: {
64777
+ ...rule,
64778
+ match: remaining
64779
+ },
64780
+ unrelatedEntries
64781
+ };
64782
+ }
64783
+ /**
64784
+ * Merge the existing rules with the generated ones. Rulesync keeps no
64785
+ * ownership record in the file, so it only removes what it is about to write
64786
+ * again: an existing rule loses the `match` entries that exactly duplicate a
64787
+ * generated (capability, pattern, effect) — a match-less rule counts as the `*`
64788
+ * pattern — and is dropped only when no entry is left.
64789
+ *
64790
+ * Everything else is kept verbatim: rules Kiro saved ("Always allow/deny"),
64791
+ * hand-written rules, rules with `exclude`, rules for capabilities rulesync
64792
+ * does not map, malformed entries. An existing rule with the same pattern but a
64793
+ * different effect is kept too, with a warning: Kiro resolves deny > ask >
64794
+ * allow, so the stricter rule still wins and a user's `deny` is never loosened.
64795
+ * Rerunning generate is therefore idempotent.
64796
+ */
64797
+ function mergeKiroCapabilityRules({ existingRules, generatedRules, logger }) {
64798
+ const emitted = /* @__PURE__ */ new Map();
64799
+ for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$6]) {
64800
+ const key = rulePatternKey(rule.capability, pattern);
64801
+ const effects = emitted.get(key) ?? /* @__PURE__ */ new Set();
64802
+ effects.add(rule.effect);
64803
+ emitted.set(key, effects);
64804
+ }
64805
+ const kept = [];
64806
+ let unrelatedManagedEntries = 0;
64807
+ for (const rule of existingRules) {
64808
+ const result = mergeExistingRule({
64809
+ rule,
64810
+ emitted,
64811
+ logger
64812
+ });
64813
+ if (result.rule !== void 0) kept.push(result.rule);
64814
+ unrelatedManagedEntries += result.unrelatedEntries;
64815
+ }
64816
+ 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.`);
64817
+ return [...kept, ...generatedRules];
64818
+ }
64819
+ const LOCATION_INDEPENDENT_FS_PATTERN = /^(?:\/|~|\*\*|[A-Za-z]:[\\/])/;
64820
+ /**
64821
+ * Permissions adapter for Kiro's capability-based `permissions.yaml` (Kiro IDE
64822
+ * 1.0, and the same engine in Kiro CLI V3). Used for the `kiro-cli` and
64823
+ * `kiro-ide` targets in **global** scope, where it writes the user-level
64824
+ * `~/.kiro/settings/permissions.yaml`. Project scope keeps writing the agent
64825
+ * config `.kiro/agents/default.json` through {@link KiroPermissions}: the
64826
+ * workspace-level `permissions.yaml` lives under a hash-derived directory in the
64827
+ * home directory, not in the repository, so it cannot be generated.
64828
+ *
64829
+ * Mapping (rulesync canonical -> Kiro rule):
64830
+ * - Category -> `capability`: `bash` -> `shell`, `read` -> `fs_read`,
64831
+ * `edit`/`write` -> `fs_write`, `webfetch` -> `web_fetch`, `websearch` ->
64832
+ * `web_search`. Other categories are skipped with a warning.
64833
+ * - Action -> `effect` 1:1, including `ask`.
64834
+ * - Pattern -> `match` entry. The catch-all `*` becomes a rule with no `match`
64835
+ * (the whole capability), because a filesystem `*` only matches one path
64836
+ * component in Kiro.
64837
+ *
64838
+ * Kiro also writes into this file (consent decisions persisted globally), so it
64839
+ * is merged, never replaced or deleted: every top-level key other than `rules`
64840
+ * is preserved, and existing rules are kept except for entries that exactly
64841
+ * repeat a generated rule (see {@link mergeKiroCapabilityRules}). Rulesync
64842
+ * never retracts a rule an earlier run wrote; it warns instead. A run that
64843
+ * changes nothing returns the existing content verbatim; otherwise the file is
64844
+ * re-serialized and its YAML comments are lost.
64845
+ *
64846
+ * @see https://kiro.dev/docs/permissions/
64847
+ */
64848
+ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPermissions {
64849
+ constructor(params) {
64850
+ super({
64851
+ ...params,
64852
+ fileContent: params.fileContent ?? ""
64853
+ });
64854
+ }
64855
+ isDeletable() {
64856
+ return false;
64857
+ }
64858
+ shouldSkipCreationWhenPayloadEmpty() {
64859
+ return true;
64860
+ }
64861
+ static getSettablePaths(_options) {
64862
+ return {
64863
+ relativeDirPath: KIRO_SETTINGS_DIR_PATH,
64864
+ relativeFilePath: KIRO_PERMISSIONS_FILE_NAME
64865
+ };
64866
+ }
64867
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
64868
+ const paths = this.getSettablePaths({ global: true });
64869
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
64870
+ return new KiroCapabilityPermissions({
64871
+ outputRoot,
64872
+ relativeDirPath: paths.relativeDirPath,
64873
+ relativeFilePath: paths.relativeFilePath,
64874
+ fileContent,
64875
+ validate,
64876
+ global: true
64877
+ });
64878
+ }
64879
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
64880
+ const paths = this.getSettablePaths({ global: true });
64881
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
64882
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
64883
+ let parsed;
64884
+ try {
64885
+ parsed = loadYaml(existingContent) ?? {};
64886
+ } catch (error) {
64887
+ throw new Error(`Failed to parse existing Kiro permissions.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
64888
+ }
64889
+ if (!isRecord$1(parsed)) throw new Error(`Failed to parse existing Kiro permissions.yaml at ${filePath}: not a map`);
64890
+ 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`);
64891
+ const config = rulesyncPermissions.getJson();
64892
+ 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.");
64893
+ const generatedRules = buildKiroCapabilityRules({
64894
+ config,
64895
+ logger
64896
+ });
64897
+ const rules = mergeKiroCapabilityRules({
64898
+ existingRules: parsed.rules ?? [],
64899
+ generatedRules,
64900
+ logger
64901
+ });
64902
+ const next = { ...parsed };
64903
+ if (rules.length > 0 || parsed.rules !== void 0) next.rules = rules;
64904
+ return new KiroCapabilityPermissions({
64905
+ outputRoot,
64906
+ relativeDirPath: paths.relativeDirPath,
64907
+ relativeFilePath: paths.relativeFilePath,
64908
+ fileContent: serializeKiroPermissions({
64909
+ next,
64910
+ parsed,
64911
+ existingContent
64912
+ }),
64913
+ validate: true,
64914
+ global: true
64915
+ });
64916
+ }
64917
+ toRulesyncPermissions() {
64918
+ let parsed;
64919
+ try {
64920
+ parsed = loadYaml(this.getFileContent()) ?? {};
64921
+ } catch (error) {
64922
+ throw new Error(`Failed to parse Kiro permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
64923
+ }
64924
+ const rules = isRecord$1(parsed) && Array.isArray(parsed.rules) ? parsed.rules : [];
64925
+ const permission = {};
64926
+ for (const rule of rules) {
64927
+ if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
64928
+ const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
64929
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$2(rule.match) ? rule.match : [];
64930
+ const categoryRules = permission[category] ??= {};
64931
+ for (const pattern of patterns) {
64932
+ const previous = categoryRules[pattern];
64933
+ if (previous === void 0 || ACTION_RANK$2[rule.effect] > ACTION_RANK$2[previous]) categoryRules[pattern] = rule.effect;
64934
+ }
64935
+ }
64936
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
64937
+ }
64938
+ validate() {
64939
+ return {
64940
+ success: true,
64941
+ error: null
64942
+ };
64943
+ }
64944
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
64945
+ return new KiroCapabilityPermissions({
64946
+ outputRoot,
64947
+ relativeDirPath,
64948
+ relativeFilePath,
64949
+ fileContent: "",
64950
+ validate: false,
64951
+ global: true
64952
+ });
64953
+ }
64954
+ };
64955
+ /**
64956
+ * Convert the canonical `permission` block into Kiro capability rules: one rule
64957
+ * per (capability, effect) carrying every non-catch-all pattern as `match`, plus
64958
+ * a match-less rule when the catch-all `*` is set.
64959
+ */
64960
+ function buildKiroCapabilityRules({ config, logger }) {
64961
+ const buckets = /* @__PURE__ */ new Map();
64962
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
64963
+ const capability = CANONICAL_TO_KIRO_CAPABILITY[category];
64964
+ if (capability === void 0) {
64965
+ logger?.warn(`Kiro permissions.yaml has no capability for category: ${category}. Skipping.`);
64966
+ continue;
64967
+ }
64968
+ for (const [pattern, effect] of Object.entries(rules)) {
64969
+ warnOnWorkspaceRelativeFsPattern({
64970
+ capability,
64971
+ category,
64972
+ pattern,
64973
+ logger
64974
+ });
64975
+ const key = `${capability}\u0000${effect}`;
64976
+ const bucket = buckets.get(key) ?? {
64977
+ capability,
64978
+ effect,
64979
+ patterns: /* @__PURE__ */ new Set()
64980
+ };
64981
+ bucket.patterns.add(pattern);
64982
+ buckets.set(key, bucket);
64983
+ }
64984
+ }
64985
+ const result = [];
64986
+ for (const { capability, effect, patterns } of buckets.values()) {
64987
+ if (patterns.has(CATCH_ALL_PATTERN$6)) result.push({
64988
+ capability,
64989
+ effect
64990
+ });
64991
+ const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$6);
64992
+ if (match.length > 0) result.push({
64993
+ capability,
64994
+ match,
64995
+ effect
64996
+ });
64997
+ }
64998
+ return result;
64999
+ }
65000
+ /**
65001
+ * Serialize the merged config. When the merge changes nothing, the existing
65002
+ * content is returned verbatim so a run with nothing to change never rewrites
65003
+ * the file (and never drops its YAML comments).
65004
+ */
65005
+ function serializeKiroPermissions({ next, parsed, existingContent }) {
65006
+ if (isDeepStrictEqual(next, parsed)) return existingContent;
65007
+ return Object.keys(next).length === 0 ? "" : dump(next);
65008
+ }
65009
+ /**
65010
+ * `~/.kiro/settings/permissions.yaml` applies to every workspace, so a relative
65011
+ * filesystem pattern such as `src/**` is not tied to the project it was written
65012
+ * for. Warn so the author can make it absolute, `~`-relative or `**`-prefixed.
65013
+ */
65014
+ function warnOnWorkspaceRelativeFsPattern({ capability, category, pattern, logger }) {
65015
+ if (capability !== "fs_read" && capability !== "fs_write") return;
65016
+ if (pattern === CATCH_ALL_PATTERN$6 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
65017
+ 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.`);
65018
+ }
65019
+ /**
65020
+ * Permissions factory for the `kiro-cli` / `kiro-ide` targets: project scope
65021
+ * writes the agent config `.kiro/agents/default.json` ({@link KiroPermissions}),
65022
+ * global scope writes `~/.kiro/settings/permissions.yaml`
65023
+ * ({@link KiroCapabilityPermissions}). The deprecated `kiro` alias stays on
65024
+ * {@link KiroPermissions} only.
65025
+ */
65026
+ const KiroScopedPermissions = {
65027
+ getSettablePaths(options = {}) {
65028
+ return options.global ? KiroCapabilityPermissions.getSettablePaths(options) : KiroPermissions.getSettablePaths(options);
65029
+ },
65030
+ fromFile(params) {
65031
+ return params.global ? KiroCapabilityPermissions.fromFile(params) : KiroPermissions.fromFile(params);
65032
+ },
65033
+ fromRulesyncPermissions(params) {
65034
+ return params.global ? KiroCapabilityPermissions.fromRulesyncPermissions(params) : KiroPermissions.fromRulesyncPermissions(params);
65035
+ },
65036
+ forDeletion(params) {
65037
+ return params.global ? KiroCapabilityPermissions.forDeletion(params) : KiroPermissions.forDeletion(params);
65038
+ }
65039
+ };
65040
+ //#endregion
63961
65041
  //#region src/features/permissions/lettacode-permissions.ts
63962
65042
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
63963
65043
  const LETTACODE_PERMISSIONS_KEY = "permissions";
@@ -65985,6 +67065,8 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
65985
67065
  "eager",
65986
67066
  "listDirectory",
65987
67067
  "todoWrite",
67068
+ "codeModeOnly",
67069
+ "freeform",
65988
67070
  "workflowsEnabled",
65989
67071
  "workflowNameOnly",
65990
67072
  "executionSandbox"
@@ -66111,6 +67193,16 @@ const QWEN_SCOPED_TOOLS_KEYS = {
66111
67193
  rule: "global-machine-wide",
66112
67194
  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.`,
66113
67195
  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."
67196
+ },
67197
+ codeModeOnly: {
67198
+ rule: "global-machine-wide",
67199
+ 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.`,
67200
+ 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."
67201
+ },
67202
+ freeform: {
67203
+ rule: "global-machine-wide",
67204
+ 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.`,
67205
+ 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."
66114
67206
  }
66115
67207
  };
66116
67208
  /**
@@ -66574,22 +67666,42 @@ function convertQwenToRulesyncPermissions(params) {
66574
67666
  //#endregion
66575
67667
  //#region src/features/permissions/reasonix-permissions.ts
66576
67668
  /**
66577
- * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
66578
- * permission-rule tool families (PascalCase).
66579
- *
66580
- * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
66581
- * documented as "Claude Code-style": "Bash and file mutation approvals use
66582
- * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
66583
- * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
66584
- * IDs for compatibility, but new rules are saved using these PascalCase
66585
- * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
66586
- * uses (the closest documented precedent for this syntax).
66587
- * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
67669
+ * Mapping from rulesync canonical tool category names (lowercase) to the tool
67670
+ * names a Reasonix `[permissions]` rule must use.
67671
+ *
67672
+ * Reasonix matches a rule's tool exactly against its registered tools, which
67673
+ * are snake_case (`read_file`, `write_file`, `grep`, `glob`, `web_fetch`,
67674
+ * `web_search`, `notebook_edit`, `task`). Only `Bash`, `Edit` (every file
67675
+ * writer: `write_file`, `edit_file`, `multi_edit`, `notebook_edit`, ...),
67676
+ * `Browser` and `Computer` are Claude-Code-style family aliases
67677
+ * (`canonicalRuleTool` / `groupOf`), so a `Read(...)` or `Write(...)` rule
67678
+ * matches nothing and Reasonix v2.34.0+ reports it as dormant. Checked on both
67679
+ * release lines: v1.39.8 (`internal/permission/permission.go`) and v2.41.0
67680
+ * (`internal/safety/permission/permission.go`, `rule_group.go`).
67681
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.41.0/internal/safety/permission/permission.go
67682
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.41.0/internal/safety/permission/rule_group.go
66588
67683
  */
66589
67684
  const CANONICAL_TO_REASONIX_TOOL_NAMES = {
66590
67685
  bash: "Bash",
66591
- read: "Read",
67686
+ read: REASONIX_READ_TOOL_NAME,
66592
67687
  edit: "Edit",
67688
+ write: "write_file",
67689
+ webfetch: "web_fetch",
67690
+ websearch: "web_search",
67691
+ grep: "grep",
67692
+ glob: "glob",
67693
+ notebookedit: "notebook_edit",
67694
+ agent: "task"
67695
+ };
67696
+ /**
67697
+ * The Claude-Code-style spellings rulesync emitted before it followed the
67698
+ * Reasonix tool vocabulary. None of them reaches a Reasonix tool, so they are
67699
+ * read back on import (a file generated by an older rulesync still imports)
67700
+ * and counted as managed on generate (the dormant copy is replaced rather than
67701
+ * kept beside the working rule).
67702
+ */
67703
+ const CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES = {
67704
+ read: REASONIX_LEGACY_READ_TOOL_NAME,
66593
67705
  write: "Write",
66594
67706
  webfetch: "WebFetch",
66595
67707
  websearch: "WebSearch",
@@ -66599,9 +67711,16 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
66599
67711
  agent: "Agent"
66600
67712
  };
66601
67713
  /**
66602
- * Reverse mapping from Reasonix tool names to rulesync canonical names.
67714
+ * Reverse mapping from Reasonix tool names (current and legacy spellings) to
67715
+ * rulesync canonical names.
66603
67716
  */
66604
- const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
67717
+ 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])]);
67718
+ /** Every Reasonix tool name a canonical category owns on generate. */
67719
+ function reasonixToolNamesOf(canonical) {
67720
+ const legacy = CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES[canonical];
67721
+ const current = toReasonixToolName(canonical);
67722
+ return legacy === void 0 ? [current] : [current, legacy];
67723
+ }
66605
67724
  function toReasonixToolName(canonical) {
66606
67725
  return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
66607
67726
  }
@@ -66874,7 +67993,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
66874
67993
  const parsed = parseReasonixConfig(existingContent);
66875
67994
  const config = rulesyncPermissions.getJson();
66876
67995
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
66877
- const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
67996
+ const managedToolNames = new Set(Object.keys(config.permission).flatMap((category) => reasonixToolNamesOf(category)));
66878
67997
  const override = config.reasonix;
66879
67998
  const rawAllow = toReasonixStringArray(override?.rawAllow);
66880
67999
  const rawAsk = toReasonixStringArray(override?.rawAsk);
@@ -66891,12 +68010,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
66891
68010
  rawOwnedEntries,
66892
68011
  logger
66893
68012
  });
66894
- if (logger && managedToolNames.has("Read")) {
68013
+ if (logger && managedToolNames.has("read_file")) {
68014
+ const readToolNames = new Set(reasonixToolNamesOf("read"));
66895
68015
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
66896
68016
  const { toolName } = parseReasonixPermissionEntry(entry);
66897
- return toolName === "Read";
68017
+ return readToolNames.has(toolName);
66898
68018
  });
66899
- 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.`);
68019
+ 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.`);
66900
68020
  }
66901
68021
  const mergedPermissions = { ...existingPermissions };
66902
68022
  setOrDeleteEntries(mergedPermissions, "allow", [
@@ -72242,18 +73362,18 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
72242
73362
  }
72243
73363
  }],
72244
73364
  ["kiro-cli", {
72245
- class: KiroPermissions,
73365
+ class: KiroScopedPermissions,
72246
73366
  meta: {
72247
73367
  supportsProject: true,
72248
- supportsGlobal: false,
73368
+ supportsGlobal: true,
72249
73369
  supportsImport: true
72250
73370
  }
72251
73371
  }],
72252
73372
  ["kiro-ide", {
72253
- class: KiroPermissions,
73373
+ class: KiroScopedPermissions,
72254
73374
  meta: {
72255
73375
  supportsProject: true,
72256
- supportsGlobal: false,
73376
+ supportsGlobal: true,
72257
73377
  supportsImport: true
72258
73378
  }
72259
73379
  }],
@@ -72504,6 +73624,335 @@ var PermissionsProcessor = class extends FeatureProcessor {
72504
73624
  }
72505
73625
  };
72506
73626
  //#endregion
73627
+ //#region src/types/tool-display.ts
73628
+ const TOOL_DISPLAY = [
73629
+ {
73630
+ key: "agentsmd",
73631
+ label: "AGENTS.md",
73632
+ group: "standard"
73633
+ },
73634
+ {
73635
+ key: "agentsskills",
73636
+ label: "AgentsSkills",
73637
+ group: "standard"
73638
+ },
73639
+ {
73640
+ key: "amp",
73641
+ label: "Amp",
73642
+ group: "ai"
73643
+ },
73644
+ {
73645
+ key: "bob",
73646
+ label: "IBM Bob",
73647
+ group: "ai"
73648
+ },
73649
+ {
73650
+ key: "claudecode",
73651
+ label: "Claude Code",
73652
+ group: "ai"
73653
+ },
73654
+ {
73655
+ key: "claudecode-plugin",
73656
+ label: "Claude Code plugin",
73657
+ group: "ai"
73658
+ },
73659
+ {
73660
+ key: "codebuddy",
73661
+ label: "CodeBuddy Code",
73662
+ group: "ai"
73663
+ },
73664
+ {
73665
+ key: "codewhale",
73666
+ label: "Codewhale",
73667
+ group: "ai"
73668
+ },
73669
+ {
73670
+ key: "codexcli",
73671
+ label: "Codex CLI",
73672
+ group: "ai"
73673
+ },
73674
+ {
73675
+ key: "commandcode",
73676
+ label: "Command Code",
73677
+ group: "ai"
73678
+ },
73679
+ {
73680
+ key: "copilot",
73681
+ label: "GitHub Copilot",
73682
+ group: "ai"
73683
+ },
73684
+ {
73685
+ key: "copilotcli",
73686
+ label: "GitHub Copilot CLI",
73687
+ group: "ai"
73688
+ },
73689
+ {
73690
+ key: "crush",
73691
+ label: "Crush",
73692
+ group: "ai"
73693
+ },
73694
+ {
73695
+ key: "goose",
73696
+ label: "Goose",
73697
+ group: "ai"
73698
+ },
73699
+ {
73700
+ key: "hermesagent",
73701
+ label: "Hermes Agent",
73702
+ group: "ai"
73703
+ },
73704
+ {
73705
+ key: "grokcli",
73706
+ label: "Grok CLI",
73707
+ group: "ai"
73708
+ },
73709
+ {
73710
+ key: "cursor",
73711
+ label: "Cursor",
73712
+ group: "ai"
73713
+ },
73714
+ {
73715
+ key: "deepagents",
73716
+ label: "deepagents-cli",
73717
+ group: "ai"
73718
+ },
73719
+ {
73720
+ key: "factorydroid",
73721
+ label: "Factory Droid",
73722
+ group: "ai"
73723
+ },
73724
+ {
73725
+ key: "opencode",
73726
+ label: "OpenCode",
73727
+ group: "ai"
73728
+ },
73729
+ {
73730
+ key: "openclaw",
73731
+ label: "OpenClaw",
73732
+ group: "ai"
73733
+ },
73734
+ {
73735
+ key: "mimocode",
73736
+ label: "MiMo Code",
73737
+ group: "ai"
73738
+ },
73739
+ {
73740
+ key: "gitlabduo",
73741
+ label: "GitLab Duo CLI",
73742
+ group: "ai"
73743
+ },
73744
+ {
73745
+ key: "cline",
73746
+ label: "Cline",
73747
+ group: "ai"
73748
+ },
73749
+ {
73750
+ key: "kilo",
73751
+ label: "Kilo Code",
73752
+ group: "ai"
73753
+ },
73754
+ {
73755
+ key: "kimi-code",
73756
+ label: "Kimi Code",
73757
+ group: "ai"
73758
+ },
73759
+ {
73760
+ key: "kimi-code-plugin",
73761
+ label: "Kimi Code plugin",
73762
+ group: "ai"
73763
+ },
73764
+ {
73765
+ key: "roo",
73766
+ label: "Roo Code ⚠️",
73767
+ group: "ai"
73768
+ },
73769
+ {
73770
+ key: "zoocode",
73771
+ label: "Zoo Code",
73772
+ group: "ai"
73773
+ },
73774
+ {
73775
+ key: "rovodev",
73776
+ label: "Rovodev (Atlassian) ⚠️",
73777
+ group: "ai"
73778
+ },
73779
+ {
73780
+ key: "takt",
73781
+ label: "Takt",
73782
+ group: "ai"
73783
+ },
73784
+ {
73785
+ key: "trae",
73786
+ label: "Trae",
73787
+ group: "ai"
73788
+ },
73789
+ {
73790
+ key: "tabnine",
73791
+ label: "Tabnine CLI ⚠️",
73792
+ group: "ai"
73793
+ },
73794
+ {
73795
+ key: "continue",
73796
+ label: "Continue ⚠️",
73797
+ group: "ai"
73798
+ },
73799
+ {
73800
+ key: "cortexcode",
73801
+ label: "Snowflake Cortex Code",
73802
+ group: "ai"
73803
+ },
73804
+ {
73805
+ key: "vibe",
73806
+ label: "Vibe Code",
73807
+ group: "ai"
73808
+ },
73809
+ {
73810
+ key: "vibe-plugin",
73811
+ label: "Vibe Code plugin",
73812
+ group: "ai"
73813
+ },
73814
+ {
73815
+ key: "qoder",
73816
+ label: "Qoder",
73817
+ group: "ai"
73818
+ },
73819
+ {
73820
+ key: "qwencode",
73821
+ label: "Qwen Code",
73822
+ group: "ai"
73823
+ },
73824
+ {
73825
+ key: "musecode",
73826
+ label: "Meta Muse Code",
73827
+ group: "ai"
73828
+ },
73829
+ {
73830
+ key: "reasonix",
73831
+ label: "Reasonix",
73832
+ group: "ai"
73833
+ },
73834
+ {
73835
+ key: "codebuff",
73836
+ label: "Codebuff (Freebuff)",
73837
+ group: "ai"
73838
+ },
73839
+ {
73840
+ key: "kiro",
73841
+ label: "Kiro ⚠️",
73842
+ group: "ai"
73843
+ },
73844
+ {
73845
+ key: "kiro-cli",
73846
+ label: "Kiro CLI",
73847
+ group: "ai"
73848
+ },
73849
+ {
73850
+ key: "kiro-ide",
73851
+ label: "Kiro IDE",
73852
+ group: "ai"
73853
+ },
73854
+ {
73855
+ key: "lettacode",
73856
+ label: "Letta Code",
73857
+ group: "ai"
73858
+ },
73859
+ {
73860
+ key: "antigravity-ide",
73861
+ label: "Google Antigravity IDE",
73862
+ group: "ai"
73863
+ },
73864
+ {
73865
+ key: "antigravity-cli",
73866
+ label: "Google Antigravity CLI",
73867
+ group: "ai"
73868
+ },
73869
+ {
73870
+ key: "antigravity-plugin",
73871
+ label: "Google Antigravity plugin",
73872
+ group: "ai"
73873
+ },
73874
+ {
73875
+ key: "aiassistant",
73876
+ label: "JetBrains AI Assistant",
73877
+ group: "ai"
73878
+ },
73879
+ {
73880
+ key: "junie",
73881
+ label: "JetBrains Junie",
73882
+ group: "ai"
73883
+ },
73884
+ {
73885
+ key: "augmentcode",
73886
+ label: "AugmentCode",
73887
+ group: "ai"
73888
+ },
73889
+ {
73890
+ key: "augmentcode-plugin",
73891
+ label: "AugmentCode plugin",
73892
+ group: "ai"
73893
+ },
73894
+ {
73895
+ key: "devin",
73896
+ label: "Devin Desktop",
73897
+ group: "ai"
73898
+ },
73899
+ {
73900
+ key: "devin-plugin",
73901
+ label: "Devin plugin",
73902
+ group: "ai"
73903
+ },
73904
+ {
73905
+ key: "warp",
73906
+ label: "Warp",
73907
+ group: "ai"
73908
+ },
73909
+ {
73910
+ key: "warpcli",
73911
+ label: "Warp Agent CLI",
73912
+ group: "ai"
73913
+ },
73914
+ {
73915
+ key: "replit",
73916
+ label: "Replit",
73917
+ group: "ai"
73918
+ },
73919
+ {
73920
+ key: "pi",
73921
+ label: "Pi Coding Agent",
73922
+ group: "ai"
73923
+ },
73924
+ {
73925
+ key: "omp",
73926
+ label: "oh-my-pi",
73927
+ group: "ai"
73928
+ },
73929
+ {
73930
+ key: "zed",
73931
+ label: "Zed",
73932
+ group: "ai"
73933
+ },
73934
+ {
73935
+ key: "zcode",
73936
+ label: "ZCode (Z.ai)",
73937
+ group: "ai"
73938
+ },
73939
+ {
73940
+ key: "zcode-plugin",
73941
+ label: "ZCode plugin",
73942
+ group: "ai"
73943
+ },
73944
+ {
73945
+ key: "pool",
73946
+ label: "Pool (Poolside)",
73947
+ group: "ai"
73948
+ },
73949
+ {
73950
+ key: "dsh",
73951
+ label: "DeepSeek Harness",
73952
+ group: "ai"
73953
+ }
73954
+ ];
73955
+ //#endregion
72507
73956
  //#region src/features/skills/simulated-skill.ts
72508
73957
  const SimulatedSkillFrontmatterSchema = z.looseObject({
72509
73958
  name: z.string(),
@@ -77911,6 +79360,24 @@ var KimiCodeSkill = class extends ToolSkill {
77911
79360
  const targets = rulesyncSkill.getFrontmatter().targets;
77912
79361
  return targets.includes("*") || targets.includes("kimi-code");
77913
79362
  }
79363
+ /**
79364
+ * Commands are emitted into the Kimi-specific skills tree as
79365
+ * `<slug>/SKILL.md` (see `KimiCodeCommand`), so a directory there matching a
79366
+ * current rulesync command slug (of a command that targets `kimi-code`) is
79367
+ * owned by the commands feature: it must not be imported as a skill nor
79368
+ * deleted as an orphan skill. A command aimed only at other tools never
79369
+ * reaches this tree, so it leaves a same-named skill alone. The import-only
79370
+ * `.agents/skills/` root never receives Kimi Code commands, so a same-named
79371
+ * skill in it stays a skill.
79372
+ */
79373
+ static async isDirOwned({ relativeDirPath, dirName, inputRoots, global }) {
79374
+ if (relativeDirPath !== this.getSettablePaths({ global }).relativeDirPath) return true;
79375
+ return !await rulesyncCommandSlugExists({
79376
+ inputRoots,
79377
+ dirName,
79378
+ toolTarget: "kimi-code"
79379
+ });
79380
+ }
77914
79381
  static async fromDir(params) {
77915
79382
  const loaded = await this.loadSkillDirContent({
77916
79383
  ...params,
@@ -77984,6 +79451,14 @@ var KimiCodePluginSkill = class extends KimiCodeSkill {
77984
79451
  return { relativeDirPath: KIMI_CODE_PLUGIN_SKILLS_DIR };
77985
79452
  }
77986
79453
  /**
79454
+ * Unlike `.kimi-code/skills/`, the plugin `skills/` tree is not shared with
79455
+ * the commands feature (`kimi-code-plugin` writes commands to `commands/`),
79456
+ * so every directory in it is a skill.
79457
+ */
79458
+ static async isDirOwned() {
79459
+ return true;
79460
+ }
79461
+ /**
77987
79462
  * The bundle is read from `--output-root <plugin>`, but the imported rulesync
77988
79463
  * file belongs to the project's `.rulesync/`, not to the plugin root.
77989
79464
  */
@@ -78335,7 +79810,8 @@ const OpenCodeSkillFrontmatterSchema = z.looseObject({
78335
79810
  license: z.optional(z.unknown()),
78336
79811
  compatibility: z.optional(z.unknown()),
78337
79812
  metadata: z.optional(z.unknown()),
78338
- "allowed-tools": z.optional(z.array(z.string()))
79813
+ "allowed-tools": z.optional(z.array(z.string())),
79814
+ "disable-model-invocation": z.optional(z.boolean())
78339
79815
  });
78340
79816
  var OpenCodeSkill = class extends ToolSkill {
78341
79817
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
@@ -78452,7 +79928,8 @@ var OpenCodeSkill = class extends ToolSkill {
78452
79928
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
78453
79929
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
78454
79930
  ...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
78455
- ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
79931
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata },
79932
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
78456
79933
  };
78457
79934
  const { toolTarget } = this.constructor.layout;
78458
79935
  const rulesyncFrontmatter = {
@@ -78487,13 +79964,18 @@ var OpenCodeSkill = class extends ToolSkill {
78487
79964
  rootFrontmatter: rulesyncFrontmatter,
78488
79965
  section: opencodeSection
78489
79966
  });
79967
+ const disableModelInvocation = resolveDisableModelInvocation({
79968
+ rootFrontmatter: rulesyncFrontmatter,
79969
+ section: opencodeSection
79970
+ });
78490
79971
  const opencodeFrontmatter = {
78491
79972
  name: rulesyncFrontmatter.name,
78492
79973
  description: rulesyncFrontmatter.description,
78493
79974
  ...license !== void 0 && { license },
78494
79975
  ...compatibility !== void 0 && { compatibility },
78495
79976
  ...metadata !== void 0 && { metadata },
78496
- ...opencodeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": opencodeSection["allowed-tools"] }
79977
+ ...opencodeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": opencodeSection["allowed-tools"] },
79978
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation }
78497
79979
  };
78498
79980
  const settablePaths = this.getSettablePaths({ global });
78499
79981
  return new this({
@@ -78516,11 +79998,13 @@ var OpenCodeSkill = class extends ToolSkill {
78516
79998
  ...params,
78517
79999
  getSettablePaths: (options) => this.getSettablePaths(options)
78518
80000
  });
78519
- const result = OpenCodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
78520
- if (!result.success) {
78521
- const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
78522
- throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
78523
- }
80001
+ const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
80002
+ const frontmatter = normalizeDisableModelInvocation({
80003
+ frontmatter: loaded.frontmatter,
80004
+ skillFilePath: join(skillDirPath, SKILL_FILE_NAME)
80005
+ });
80006
+ const result = OpenCodeSkillFrontmatterSchema.safeParse(frontmatter);
80007
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
78524
80008
  return new this({
78525
80009
  outputRoot: loaded.outputRoot,
78526
80010
  relativeDirPath: loaded.relativeDirPath,
@@ -78548,6 +80032,52 @@ var OpenCodeSkill = class extends ToolSkill {
78548
80032
  });
78549
80033
  }
78550
80034
  };
80035
+ const DISABLE_MODEL_INVOCATION_KEY = "disable-model-invocation";
80036
+ /**
80037
+ * Reads a `disable-model-invocation` value the way OpenCode V2 does
80038
+ * (`booleanValue` in `packages/core/src/config/plugin/skill-file.ts`): a
80039
+ * boolean, the numbers `1` / `0`, or a `true`/`yes`/`on`/`1` /
80040
+ * `false`/`no`/`off`/`0` string compared after trimming and case-folding.
80041
+ * Anything else is `undefined`, which OpenCode ignores.
80042
+ */
80043
+ function parseOpencodeBoolean(value) {
80044
+ if (typeof value === "boolean") return value;
80045
+ if (typeof value === "number") return value === 1 ? true : value === 0 ? false : void 0;
80046
+ if (typeof value !== "string") return;
80047
+ const normalized = value.trim().toLowerCase();
80048
+ if ([
80049
+ "true",
80050
+ "yes",
80051
+ "on",
80052
+ "1"
80053
+ ].includes(normalized)) return true;
80054
+ if ([
80055
+ "false",
80056
+ "no",
80057
+ "off",
80058
+ "0"
80059
+ ].includes(normalized)) return false;
80060
+ }
80061
+ /**
80062
+ * Coerces an imported `disable-model-invocation` onto the boolean the schema
80063
+ * types. A spelling OpenCode cannot read is dropped with a warning instead of
80064
+ * failing the import, since OpenCode itself ignores it.
80065
+ */
80066
+ function normalizeDisableModelInvocation({ frontmatter, skillFilePath }) {
80067
+ if (!Object.hasOwn(frontmatter, DISABLE_MODEL_INVOCATION_KEY)) return frontmatter;
80068
+ const raw = frontmatter[DISABLE_MODEL_INVOCATION_KEY];
80069
+ const parsed = parseOpencodeBoolean(raw);
80070
+ if (parsed === raw) return frontmatter;
80071
+ const { [DISABLE_MODEL_INVOCATION_KEY]: _dropped, ...rest } = frontmatter;
80072
+ if (parsed === void 0) {
80073
+ warnOnceWithFallback(void 0, `${stripControlCharacters(toPosixPath(skillFilePath))}: ignoring unreadable "${DISABLE_MODEL_INVOCATION_KEY}" value ${stripControlCharacters(JSON.stringify(raw) ?? String(raw))}; OpenCode ignores it too.`);
80074
+ return rest;
80075
+ }
80076
+ return {
80077
+ ...rest,
80078
+ [DISABLE_MODEL_INVOCATION_KEY]: parsed
80079
+ };
80080
+ }
78551
80081
  //#endregion
78552
80082
  //#region src/features/skills/mimocode-skill.ts
78553
80083
  /**
@@ -84849,7 +86379,7 @@ var DeepagentsSubagent = class DeepagentsSubagent extends ToolSubagent {
84849
86379
  //#endregion
84850
86380
  //#region src/features/subagents/devin-subagent.ts
84851
86381
  const DevinSubagentFrontmatterSchema = z.looseObject({
84852
- name: z.string(),
86382
+ name: z.optional(z.string()),
84853
86383
  description: z.optional(z.string()),
84854
86384
  model: z.optional(z.string()),
84855
86385
  "allowed-tools": z.optional(z.array(z.string())),
@@ -84902,7 +86432,7 @@ var DevinSubagent = class extends ToolSubagent {
84902
86432
  outputRoot: ".",
84903
86433
  frontmatter: {
84904
86434
  targets: ["*"],
84905
- name,
86435
+ name: name ?? this.getSubagentName(),
84906
86436
  description,
84907
86437
  ...Object.keys(devinSection).length > 0 && { devin: devinSection }
84908
86438
  },
@@ -84976,7 +86506,8 @@ var DevinSubagent = class extends ToolSubagent {
84976
86506
  const paths = this.getSettablePaths({ global });
84977
86507
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
84978
86508
  const fileContent = await readFileContent(filePath);
84979
- const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
86509
+ const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
86510
+ if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}`);
84980
86511
  const result = DevinSubagentFrontmatterSchema.safeParse(frontmatter);
84981
86512
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
84982
86513
  return new this({
@@ -98624,6 +100155,7 @@ var RulesProcessor = class extends FeatureProcessor {
98624
100155
  warnForOutputStyleRuleOptions({ rules, factory }) {
98625
100156
  const toolTarget = this.toolTarget;
98626
100157
  if (toolTarget !== "factorydroid" && toolTarget !== "qoder") return;
100158
+ const toolLabel = TOOL_DISPLAY.find((entry) => entry.key === toolTarget)?.label ?? toolTarget;
98627
100159
  for (const rule of rules) {
98628
100160
  const frontmatter = rule.getFrontmatter();
98629
100161
  const section = frontmatter[toolTarget];
@@ -98631,7 +100163,7 @@ var RulesProcessor = class extends FeatureProcessor {
98631
100163
  const { channel, name } = section;
98632
100164
  const fileName = stripControlCharacters(rule.getRelativeFilePath());
98633
100165
  const targets = frontmatter.targets ?? ["*"];
98634
- if (channel === "output-style" && targets.some((target) => target !== toolTarget)) this.logger.warn(`${fileName} sets ${toolTarget}.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["${toolTarget}"]\` to keep the output style to ${toolTarget === "qoder" ? "Qoder" : "Factory Droid"}.`);
100166
+ if (channel === "output-style" && targets.some((target) => target !== toolTarget)) this.logger.warn(`${fileName} sets ${toolTarget}.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["${toolTarget}"]\` to keep the output style to ${toolLabel}.`);
98635
100167
  if (channel !== "output-style" && name !== void 0) this.logger.warn(`${fileName} sets ${toolTarget}.name, which is read only with ${toolTarget}.channel: output-style; ignoring it.`);
98636
100168
  }
98637
100169
  }
@@ -100051,13 +101583,15 @@ const ANTIGRAVITY_PLUGIN_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/;
100051
101583
  * The manifest declares only `name` and `description`, and rejects anything
100052
101584
  * else, so the one required field is derived from the plugin directory's name.
100053
101585
  * An existing manifest is never read or rewritten: it may carry a
100054
- * hand-authored `name` or `description` that rulesync has no source for.
101586
+ * hand-authored `name` or `description` that rulesync has no source for. A
101587
+ * manifest kept in one of the manifest folders counts as present too, so no
101588
+ * root stub is written next to it.
100055
101589
  */
100056
101590
  async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger }) {
100057
101591
  const paths = [];
100058
101592
  for (const outputRoot of outputRoots) {
100059
101593
  const filePath = join(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME);
100060
- if (await fileExists(filePath)) continue;
101594
+ if (await hasAntigravityPluginManifest(outputRoot)) continue;
100061
101595
  if (await refusesWriteOutsideRoot({
100062
101596
  logger,
100063
101597
  rootPath: outputRoot,
@@ -100080,6 +101614,11 @@ async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger })
100080
101614
  sourceLoadFailed: false
100081
101615
  };
100082
101616
  }
101617
+ async function hasAntigravityPluginManifest(outputRoot) {
101618
+ const candidates = [join(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME), ...ANTIGRAVITY_PLUGIN_MANIFEST_FOLDERS.map((folder) => join(outputRoot, folder, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME))];
101619
+ for (const candidate of candidates) if (await fileExists(candidate)) return true;
101620
+ return false;
101621
+ }
100083
101622
  //#endregion
100084
101623
  //#region src/features/shared/hermes-project-plugin-activation.ts
100085
101624
  function mergeEnabledPlugins({ existingContent, filePath, pluginNames }) {
@@ -102377,6 +103916,6 @@ async function importChecksCore(params) {
102377
103916
  return writtenCount;
102378
103917
  }
102379
103918
  //#endregion
102380
- export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, ALL_FEATURES as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_MCP_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, hasDeceptiveHiddenCharacters as Gn, isSymlink as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, DEPRECATED_FEATURE_REPLACEMENTS as Hn, hasExecutableBit as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as In, directoryExists as It, RulesyncRule as J, stripControlCharacters as Jn, listSubdirectoryNames as Jt, RulesyncSkill as K, hasEnclosingMarkOutsideKeycap as Kn, listDirectoryEntryNames as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_USER_CONFIG_DIR_NAME as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RULES_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_SCHEMA_URL as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, formatError as Un, isFileNotFoundError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES_WITH_WILDCARD as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, truncateText as Wn, isFileSystemError as Wt, RulesyncPermissions as X, stripHiddenCharacters as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharactersKeepingLineFeeds as Yn, pathEscapesRoot as Yt, RulesyncMcp as Z, readFileContentOrNull as Zt, LETTACODE_DIR as _, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CONFIG_RELATIVE_FILE_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS_WITH_WILDCARD as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, PACKAGING_TOOL_TARGETS as fn, mergeInputRootConfigs as ft, HooksProcessor as g, MAX_FILE_SIZE as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, FEATURE_SOURCE_TREE_ENTRIES as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedPathEscapesRoot as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_SCHEMA_URL as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, CURATED_RULES_FEATURE_SUBDIR as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, ToolTargetSchema as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, quoteForLog as qn, listFilePathsRecursively as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, ALL_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CHECKS_RELATIVE_DIR_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_HOOKS_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CONFIG_SCHEMA_URL as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, parseCommaSeparatedList as zn, findFilesByGlobs as zt };
103919
+ export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, LETTACODE_DIR as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, mapWithConcurrency as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
102381
103920
 
102382
- //# sourceMappingURL=import-DFwX4TTE.js.map
103921
+ //# sourceMappingURL=import-Tya9qZRK.js.map