rulesync 43.5.0 → 43.6.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.
@@ -382,36 +382,36 @@ function formatError(error) {
382
382
  return boundErrorMessage(String(error));
383
383
  }
384
384
  // src/constants/rulesync-paths.ts
385
- const { join: join$384 } = node_path.posix;
385
+ const { join: join$385 } = node_path.posix;
386
386
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
387
387
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
388
388
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
389
389
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
390
390
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
391
391
  const RULES_FEATURE_SUBDIR = "rules";
392
- const CURATED_RULES_FEATURE_SUBDIR = join$384(RULES_FEATURE_SUBDIR, ".curated");
392
+ const CURATED_RULES_FEATURE_SUBDIR = join$385(RULES_FEATURE_SUBDIR, ".curated");
393
393
  const COMMANDS_FEATURE_SUBDIR = "commands";
394
394
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
395
395
  const CHECKS_FEATURE_SUBDIR = "checks";
396
396
  const SKILLS_FEATURE_SUBDIR = "skills";
397
- const CURATED_SKILLS_FEATURE_SUBDIR = join$384(SKILLS_FEATURE_SUBDIR, ".curated");
398
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
399
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
400
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
401
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
402
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
403
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
404
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
405
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
406
- join$384(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
407
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
408
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
397
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$385(SKILLS_FEATURE_SUBDIR, ".curated");
398
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
399
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
400
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
401
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
402
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
403
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
404
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
405
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
406
+ join$385(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
407
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
408
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
409
409
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
410
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
410
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
411
411
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
412
412
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
413
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
414
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
413
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
414
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
415
415
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
416
416
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
417
417
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -895,6 +895,7 @@ const hooksProcessorToolTargetTuple = [
895
895
  "qwencode",
896
896
  "reasonix",
897
897
  "tabnine",
898
+ "trae",
898
899
  "grokcli",
899
900
  "gitlabduo",
900
901
  "lettacode",
@@ -3945,6 +3946,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3945
3946
  ".poolside/settings.yaml",
3946
3947
  ".config/poolside/settings.yaml",
3947
3948
  ".tabnine/agent/settings.json",
3949
+ ".trae/hooks.json",
3948
3950
  ".vibe/config.toml",
3949
3951
  ".zcode/config.json",
3950
3952
  ".zcode/cli/config.json",
@@ -6027,6 +6029,34 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
6027
6029
  };
6028
6030
  const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CORTEXCODE_EVENT_NAMES);
6029
6031
  /**
6032
+ * Hook events supported by TRAE.
6033
+ *
6034
+ * TRAE reads `.trae/hooks.json` (project) and `~/.trae/hooks.json` (user):
6035
+ * `{ "version"?: 1, "hooks": { "<Event>": [{ "matcher"?, "hooks": [{ "type":
6036
+ * "command", "command", "timeout"? }] }] } }` with Claude-Code-style
6037
+ * PascalCase event names. `$TRAE_PROJECT_DIR` resolves to the workspace root
6038
+ * inside commands.
6039
+ *
6040
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
6041
+ */
6042
+ const TRAE_HOOK_EVENTS = [
6043
+ "sessionStart",
6044
+ "beforeSubmitPrompt",
6045
+ "preToolUse",
6046
+ "postToolUse",
6047
+ "stop",
6048
+ "notification"
6049
+ ];
6050
+ const CANONICAL_TO_TRAE_EVENT_NAMES = {
6051
+ sessionStart: "SessionStart",
6052
+ beforeSubmitPrompt: "UserPromptSubmit",
6053
+ preToolUse: "PreToolUse",
6054
+ postToolUse: "PostToolUse",
6055
+ stop: "Stop",
6056
+ notification: "Notification"
6057
+ };
6058
+ const TRAE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_TRAE_EVENT_NAMES);
6059
+ /**
6030
6060
  * Hook events supported by CodeBuddy Code.
6031
6061
  *
6032
6062
  * CodeBuddy Code reads hooks from the `hooks` key of `.codebuddy/settings.json`
@@ -6499,6 +6529,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
6499
6529
  bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6500
6530
  codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6501
6531
  cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6532
+ trae: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6502
6533
  commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6503
6534
  lettacode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6504
6535
  continue: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
@@ -14181,6 +14212,14 @@ const SHARED_CONFIG_OWNERSHIP = {
14181
14212
  }
14182
14213
  }
14183
14214
  },
14215
+ ".trae/hooks.json": {
14216
+ format: "json",
14217
+ invalidRootPolicy: "error",
14218
+ features: { hooks: {
14219
+ kind: "replace-owned-keys",
14220
+ ownedKeys: ["hooks"]
14221
+ } }
14222
+ },
14184
14223
  ".trae/permission/global.json": {
14185
14224
  format: "json",
14186
14225
  invalidRootPolicy: "error",
@@ -23306,10 +23345,13 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23306
23345
  * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23307
23346
  * @see https://docs.trae.ai/ide/ignore-files?_lang=en
23308
23347
  * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
23348
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
23309
23349
  */
23310
23350
  const TRAE_DIR = ".trae";
23311
23351
  /** Project MCP servers (`mcpServers` object), project scope only. */
23312
23352
  const TRAE_MCP_FILE_NAME = "mcp.json";
23353
+ /** Hooks: `.trae/hooks.json` (project) and `~/.trae/hooks.json` (global). */
23354
+ const TRAE_HOOKS_FILE_NAME = "hooks.json";
23313
23355
  /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
23314
23356
  const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
23315
23357
  /**
@@ -25234,12 +25276,80 @@ var AiassistantSkill = class AiassistantSkill extends ToolSkill {
25234
25276
  });
25235
25277
  }
25236
25278
  };
25279
+ // src/features/shared/amp-settings.ts
25280
+ /**
25281
+ * Parse an Amp `settings.json` / `settings.jsonc` document.
25282
+ *
25283
+ * Shared by the MCP and permissions adapters because both read the very same
25284
+ * file. Two separately maintained copies drifted once already: the permissions
25285
+ * side was hardened while the MCP side kept calling `jsonc-parser` directly,
25286
+ * which left it importing MCP servers reachable only through an injected
25287
+ * `__proto__`.
25288
+ *
25289
+ * {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
25290
+ * error instead of returning `jsonc-parser`'s best-effort value, and it
25291
+ * rebuilds every object from its own enumerable entries, so a root `__proto__`
25292
+ * cannot swap the returned object's prototype and `constructor` / `prototype`
25293
+ * never survive as own keys, at any depth.
25294
+ *
25295
+ * Removal is silent, matching every other tool-side adapter (opencode, kilo,
25296
+ * copilot). Reporting the removed keys through `droppedPollutionKeysError` is
25297
+ * reserved for the three files a user authors under `.rulesync/`, whose whole
25298
+ * purpose is to be turned into tool config — a key that vanishes there needs
25299
+ * explaining, whereas here the surrounding settings are the user's own file
25300
+ * and are left alone.
25301
+ *
25302
+ * Note the consequence for a root `__proto__`: sanitizing runs before the
25303
+ * plain-object check, so such a document parses successfully with the key
25304
+ * stripped rather than failing that check. That is deliberate — dropping the
25305
+ * one poisoned key keeps the user's unrelated settings — and matches the
25306
+ * sanitize-before-the-root-check design the shared config gateway documents.
25307
+ */
25308
+ function parseAmpSettings({ fileContent }) {
25309
+ let parsed;
25310
+ try {
25311
+ parsed = parseJsonc$6(fileContent || "{}");
25312
+ } catch (error) {
25313
+ throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
25314
+ }
25315
+ if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
25316
+ return parsed;
25317
+ }
25318
+ /**
25319
+ * Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
25320
+ * an existing user file is read-modified-written in place instead of a fresh
25321
+ * `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
25322
+ * `settings.json` when neither file exists: Amp's canonically documented
25323
+ * settings file (`.amp/settings.json`), shared by the MCP and permissions
25324
+ * adapters, so generating both on a clean repo writes a single settings file.
25325
+ */
25326
+ async function resolveAmpSettingsFile(jsonDir) {
25327
+ const jsoncContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
25328
+ if (jsoncContent !== null) return {
25329
+ fileContent: jsoncContent,
25330
+ relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
25331
+ };
25332
+ const jsonContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_FILE_NAME));
25333
+ if (jsonContent !== null) return {
25334
+ fileContent: jsonContent,
25335
+ relativeFilePath: AMP_SETTINGS_FILE_NAME
25336
+ };
25337
+ return {
25338
+ fileContent: null,
25339
+ relativeFilePath: AMP_SETTINGS_FILE_NAME
25340
+ };
25341
+ }
25237
25342
  // src/features/skills/amp-skill.ts
25238
25343
  const AmpSkillFrontmatterSchema = zod_mini.z.looseObject({
25239
25344
  name: zod_mini.z.string(),
25240
25345
  description: zod_mini.z.string()
25241
25346
  });
25242
25347
  /**
25348
+ * Amp setting that makes it skip `~/.config/agents/skills/` and
25349
+ * `~/.agents/skills/`. https://ampcode.com/docs/cli/settings
25350
+ */
25351
+ const AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY = "amp.skills.disableGlobalAgentsSkills";
25352
+ /**
25243
25353
  * Represents an Amp Agent Skill directory.
25244
25354
  *
25245
25355
  * Amp natively reads Agent Skills following the open standard: a directory with a
@@ -25274,6 +25384,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
25274
25384
  ...global && { importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH, AMP_SKILLS_NATIVE_GLOBAL_DIR] }
25275
25385
  };
25276
25386
  }
25387
+ /**
25388
+ * Warn when the user's Amp settings disable the global agent-agnostic skill
25389
+ * roots, because the global skills rulesync writes to
25390
+ * `~/.config/agents/skills/` would then never load. Only the user settings
25391
+ * file is checked: a workspace `.amp/settings.json(c)` depends on where Amp
25392
+ * is started, which global generation cannot know.
25393
+ */
25394
+ static async getGenerationWarnings({ outputRoot, global, logger }) {
25395
+ if (!global) return [];
25396
+ const { fileContent, relativeFilePath } = await resolveAmpSettingsFile((0, node_path.join)(outputRoot, AMP_GLOBAL_DIR));
25397
+ if (fileContent === null) return [];
25398
+ let settings;
25399
+ try {
25400
+ settings = parseAmpSettings({ fileContent });
25401
+ } catch (error) {
25402
+ logger.debug(`Skipping the Amp skills settings check for ${(0, node_path.join)(AMP_GLOBAL_DIR, relativeFilePath)}: ${formatError(error)}`);
25403
+ return [];
25404
+ }
25405
+ if (settings[AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY] !== true) return [];
25406
+ return [`Amp will not load the global skills rulesync writes to ~/${AMP_SKILLS_GLOBAL_DIR}/: '${AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY}' is true in ~/${(0, node_path.join)(AMP_GLOBAL_DIR, relativeFilePath)}.`];
25407
+ }
25277
25408
  getFrontmatter() {
25278
25409
  return AmpSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
25279
25410
  }
@@ -33250,7 +33381,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
33250
33381
  async convertRulesyncDirsToToolDirs(rulesyncDirs) {
33251
33382
  const rulesyncSkills = rulesyncDirs.filter((dir) => dir instanceof RulesyncSkill);
33252
33383
  const factory = this.getFactory(this.toolTarget);
33253
- return (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
33384
+ const toolSkills = (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
33254
33385
  const isClaudecodeScheduledTask = rulesyncSkill.getFrontmatter().claudecode?.["scheduled-task"] === true;
33255
33386
  if (isClaudecodeScheduledTask && this.toolTarget !== "claudecode" && this.toolTarget !== "claudecode-legacy") return null;
33256
33387
  if (!factory.class.isTargetedByRulesyncSkill(rulesyncSkill)) return null;
@@ -33277,6 +33408,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
33277
33408
  logger: this.logger
33278
33409
  });
33279
33410
  }))).filter((skill) => skill !== null);
33411
+ if (toolSkills.length > 0 && factory.class.getGenerationWarnings) {
33412
+ const warnings = await factory.class.getGenerationWarnings({
33413
+ outputRoot: this.outputRoot,
33414
+ global: this.global,
33415
+ logger: this.logger
33416
+ });
33417
+ for (const warning of warnings) this.logger.warn(warning);
33418
+ }
33419
+ return toolSkills;
33280
33420
  }
33281
33421
  async convertToolDirsToRulesyncDirs(toolDirs) {
33282
33422
  const toolSkills = toolDirs.filter((dir) => dir instanceof ToolSkill);
@@ -47482,6 +47622,45 @@ var DshRule = class DshRule extends NestedAgentsmdRule {
47482
47622
  };
47483
47623
  }
47484
47624
  };
47625
+ // src/features/rules/output-style-paths.ts
47626
+ /**
47627
+ * Shared helpers for tool rule adapters that write opted-in rules as custom
47628
+ * output styles (`<tool dir>/output-styles/<name>.md`), e.g. Qoder and
47629
+ * Factory Droid. Both tools read only the direct `.md` children of their
47630
+ * output-styles directory, with optional `name` / `description` frontmatter.
47631
+ */
47632
+ /**
47633
+ * Whether the given path is a direct `.md` child of `outputStylesDirPath`.
47634
+ */
47635
+ function isDirectMarkdownChild({ outputStylesDirPath, relativeDirPath, relativeFilePath }) {
47636
+ return relativeDirPath === outputStylesDirPath && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
47637
+ }
47638
+ /**
47639
+ * Whether a rule opts into the `output-style` channel: only a non-root rule
47640
+ * can, since an output style is a file of its own.
47641
+ */
47642
+ function isOutputStyleChannel({ root, channel }) {
47643
+ return !root && channel === "output-style";
47644
+ }
47645
+ /**
47646
+ * Return the file stem of an output-style source rule, throwing `message`
47647
+ * when the rule is nested in a subdirectory of the rules directory: the
47648
+ * output file is named after the rule, and the tools read only the top level
47649
+ * of their output-styles directory.
47650
+ */
47651
+ function resolveTopLevelOutputStyleStem({ sourcePath, message }) {
47652
+ if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(message);
47653
+ return (0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath));
47654
+ }
47655
+ /**
47656
+ * The output style's frontmatter: only the `name` and `description` that are set.
47657
+ */
47658
+ function buildOutputStyleFrontmatter({ name, description }) {
47659
+ return {
47660
+ ...name !== void 0 && { name },
47661
+ ...description !== void 0 && { description }
47662
+ };
47663
+ }
47485
47664
  // src/features/rules/factorydroid-rule.ts
47486
47665
  /**
47487
47666
  * The file-name glob `getExtraFixedFiles` returns for the output-styles
@@ -47586,7 +47765,11 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47586
47765
  * child of the output-styles directory, at either scope.
47587
47766
  */
47588
47767
  static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
47589
- return relativeDirPath === this.getOutputStylesDirPath() && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
47768
+ return isDirectMarkdownChild({
47769
+ outputStylesDirPath: this.getOutputStylesDirPath(),
47770
+ relativeDirPath,
47771
+ relativeFilePath
47772
+ });
47590
47773
  }
47591
47774
  /**
47592
47775
  * Which fixed-file channel, if any, owns the given output path. Matching on
@@ -47626,7 +47809,10 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47626
47809
  /** Whether a rule opts into the `output-style` channel (non-root only). */
47627
47810
  static isOutputStyleRule(rulesyncRule) {
47628
47811
  const frontmatter = rulesyncRule.getFrontmatter();
47629
- return !frontmatter.root && frontmatter.factorydroid?.channel === "output-style";
47812
+ return isOutputStyleChannel({
47813
+ root: frontmatter.root,
47814
+ channel: frontmatter.factorydroid?.channel
47815
+ });
47630
47816
  }
47631
47817
  /**
47632
47818
  * Factory Droid loads the channel files itself, so listing one in the root
@@ -47758,15 +47944,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47758
47944
  static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
47759
47945
  const frontmatter = rulesyncRule.getFrontmatter();
47760
47946
  const sourcePath = rulesyncRule.getRelativeFilePath();
47761
- if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(`Rule '${sourcePath}' sets factorydroid.channel: output-style but is nested in a subdirectory: Factory Droid loads only direct .md children of .factory/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
47762
- const stem = (0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath));
47947
+ const stem = resolveTopLevelOutputStyleStem({
47948
+ sourcePath,
47949
+ message: `Rule '${sourcePath}' sets factorydroid.channel: output-style but is nested in a subdirectory: Factory Droid loads only direct .md children of .factory/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
47950
+ });
47763
47951
  const name = frontmatter.factorydroid?.name;
47764
47952
  const styleName = name ?? stem;
47765
47953
  if (FACTORYDROID_RESERVED_OUTPUT_STYLE_NAMES.includes(styleName.trim().toLowerCase())) throw new Error(`Rule '${sourcePath}' sets factorydroid.channel: output-style with the style name '${styleName}', which is reserved: Factory Droid's built-in Default and Concise styles cannot be replaced. Rename the rule file or set factorydroid.name.`);
47766
- const styleFrontmatter = {
47767
- ...name !== void 0 && { name },
47768
- ...frontmatter.description !== void 0 && { description: frontmatter.description }
47769
- };
47954
+ const styleFrontmatter = buildOutputStyleFrontmatter({
47955
+ name,
47956
+ description: frontmatter.description
47957
+ });
47770
47958
  const body = rulesyncRule.getBody();
47771
47959
  return new FactorydroidRule({
47772
47960
  outputRoot,
@@ -49402,12 +49590,19 @@ var QoderRule = class QoderRule extends ToolRule {
49402
49590
  * `.qoder/output-styles/`, at either scope.
49403
49591
  */
49404
49592
  static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
49405
- return relativeDirPath === QODER_OUTPUT_STYLES_DIR_PATH && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
49593
+ return isDirectMarkdownChild({
49594
+ outputStylesDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
49595
+ relativeDirPath,
49596
+ relativeFilePath
49597
+ });
49406
49598
  }
49407
49599
  /** Whether a rule opts into the `output-style` channel (non-root only). */
49408
49600
  static isOutputStyleRule(rulesyncRule) {
49409
49601
  const frontmatter = rulesyncRule.getFrontmatter();
49410
- return !frontmatter.root && frontmatter.qoder?.channel === "output-style";
49602
+ return isOutputStyleChannel({
49603
+ root: frontmatter.root,
49604
+ channel: frontmatter.qoder?.channel
49605
+ });
49411
49606
  }
49412
49607
  /**
49413
49608
  * The output-styles directory, enumerated for import only: it also holds
@@ -49518,16 +49713,18 @@ var QoderRule = class QoderRule extends ToolRule {
49518
49713
  static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
49519
49714
  const { description, qoder } = rulesyncRule.getFrontmatter();
49520
49715
  const sourcePath = rulesyncRule.getRelativeFilePath();
49521
- if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(`Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
49522
- const name = qoder?.name;
49716
+ const stem = resolveTopLevelOutputStyleStem({
49717
+ sourcePath,
49718
+ message: `Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
49719
+ });
49523
49720
  return new QoderRule({
49524
49721
  outputRoot,
49525
49722
  relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
49526
- relativeFilePath: `${(0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath))}.md`,
49527
- frontmatter: {
49528
- ...name !== void 0 && { name },
49529
- ...description !== void 0 && { description }
49530
- },
49723
+ relativeFilePath: `${stem}.md`,
49724
+ frontmatter: buildOutputStyleFrontmatter({
49725
+ name: qoder?.name,
49726
+ description
49727
+ }),
49531
49728
  body: rulesyncRule.getBody(),
49532
49729
  validate,
49533
49730
  root: false,
@@ -56205,9 +56402,9 @@ function groupDefinitionsByMatcher({ definitions, eventName, converterConfig, wa
56205
56402
  });
56206
56403
  translatedMatchers.set(normalizedMatcher, matcher);
56207
56404
  }
56208
- const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
56405
+ const key = [matcher, ...subdividingFields.map(({ canonical, valueType, events }) => {
56209
56406
  const value = def[canonical];
56210
- return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
56407
+ return isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
56211
56408
  })].join("\0");
56212
56409
  const group = byMatcher.get(key);
56213
56410
  if (group) group.defs.push(def);
@@ -56248,6 +56445,7 @@ function runsInPosixShell({ shell, converterConfig }) {
56248
56445
  * Apply the optional project directory variable prefix to a command string.
56249
56446
  */
56250
56447
  function applyCommandPrefix({ def, converterConfig }) {
56448
+ if (converterConfig.stripsProjectDirVarOnImportOnly) return def.command;
56251
56449
  const commandText = def.command;
56252
56450
  const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
56253
56451
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
@@ -56456,12 +56654,16 @@ const describeInvalidObject = ({ tool, canonical, value }) => isPlainObject(valu
56456
56654
  value
56457
56655
  })} Importing it would fail validation on the next run.` : `Dropping "${tool}" while importing a hook: it must be an object.`;
56458
56656
  const describeInvalidRecord = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a map of strings whose keys are non-empty and free of "=", and whose keys and values carry no newline, carriage return or NUL characters.`;
56657
+ function isGroupFieldOnEvent(events, eventName) {
56658
+ return events === void 0 || events.has(eventName);
56659
+ }
56459
56660
  /**
56460
56661
  * Check a value against the shape its field documents. A string field also
56461
56662
  * rejects control characters, matching the canonical `safeString` so an
56462
56663
  * imported value cannot fail validation on the next generate.
56463
56664
  */
56464
56665
  function isGroupPassthroughValue(value, valueType = "object") {
56666
+ if (valueType === "number") return typeof value === "number" && Number.isFinite(value);
56465
56667
  if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
56466
56668
  return isPlainObject(value);
56467
56669
  }
@@ -56471,10 +56673,14 @@ function isGroupPassthroughValue(value, valueType = "object") {
56471
56673
  */
56472
56674
  function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
56473
56675
  const emitted = {};
56474
- for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
56676
+ for (const { canonical, tool, valueType, events } of converterConfig.groupPassthroughFields ?? []) {
56475
56677
  const carried = defs.map((def) => def[canonical]);
56476
56678
  const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
56477
56679
  if (first === void 0) continue;
56680
+ if (!isGroupFieldOnEvent(events, eventName)) {
56681
+ logger?.warn(`"${canonical}" on "${eventName}" hooks is ignored — the tool supports "${tool}" only on ${[...events ?? []].map((event) => `"${event}"`).join(", ")} hooks.`);
56682
+ continue;
56683
+ }
56478
56684
  const firstStable = stableJson(first);
56479
56685
  const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
56480
56686
  if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${quoteValueForWarning(first)} — including any that asked for something else, or for nothing.`);
@@ -56486,9 +56692,9 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
56486
56692
  * Import the configured group-level passthrough fields onto a definition,
56487
56693
  * reversing {@link emitGroupPassthroughFields}.
56488
56694
  */
56489
- function importGroupPassthroughFields({ rawEntry, converterConfig }) {
56695
+ function importGroupPassthroughFields({ rawEntry, eventName, converterConfig }) {
56490
56696
  const entry = rawEntry;
56491
- return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
56697
+ return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType, events }) => isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
56492
56698
  }
56493
56699
  /**
56494
56700
  * Emit the payload fields specific to a hook type — `url`/`headers`/
@@ -56814,19 +57020,23 @@ function stripCommandPrefix({ command, args, shell, converterConfig, warn }) {
56814
57020
  const cmd = typeof command === "string" ? command : void 0;
56815
57021
  const { projectDirVar } = converterConfig;
56816
57022
  if (projectDirVar === "" || typeof cmd !== "string") return cmd;
56817
- const { command: stripped, unrestored, retained } = importProjectDirVariable({
56818
- command: cmd,
56819
- projectDirVar,
56820
- firstWordOnly: isExecFormHook({
56821
- args,
56822
- converterConfig
56823
- }) || !runsInPosixShell({
56824
- shell,
56825
- converterConfig
56826
- })
56827
- });
56828
- if (warn && unrestored) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${projectDirVar} path that is not a script the command runs becomes relative to the hook's working directory and is not restored on generate. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") to keep the variable.`);
56829
- if (warn && retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${projectDirVar} in an assignment or option value, or in a later word of a command whose later words are not normalized, so the imported command still refers to it and other tools that do not define it receive it verbatim. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") if other tools should not see it.`);
57023
+ let stripped = cmd;
57024
+ for (const variable of [projectDirVar, ...converterConfig.importedProjectDirVarAliases ?? []]) {
57025
+ const result = importProjectDirVariable({
57026
+ command: stripped,
57027
+ projectDirVar: variable,
57028
+ firstWordOnly: isExecFormHook({
57029
+ args,
57030
+ converterConfig
57031
+ }) || !runsInPosixShell({
57032
+ shell,
57033
+ converterConfig
57034
+ })
57035
+ });
57036
+ stripped = result.command;
57037
+ if (warn && result.unrestored && !converterConfig.stripsProjectDirVarOnImportOnly) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${variable} path that is not a script the command runs becomes relative to the hook's working directory and is not restored on generate. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") to keep the variable.`);
57038
+ if (warn && result.retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${variable} in an assignment or option value, or in a later word of a command whose later words are not normalized, so the imported command still refers to it and other tools that do not define it receive it verbatim. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") if other tools should not see it.`);
57039
+ }
56830
57040
  return stripped;
56831
57041
  }
56832
57042
  /**
@@ -56867,7 +57077,7 @@ function isStringRecord(value) {
56867
57077
  }
56868
57078
  /** Compare object values without letting key order decide the answer. */
56869
57079
  function stableJson(value) {
56870
- if (typeof value === "string") return JSON.stringify(value);
57080
+ if (typeof value !== "object") return JSON.stringify(value);
56871
57081
  return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
56872
57082
  }
56873
57083
  /**
@@ -57107,6 +57317,7 @@ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn })
57107
57317
  }),
57108
57318
  ...importGroupPassthroughFields({
57109
57319
  rawEntry,
57320
+ eventName,
57110
57321
  converterConfig
57111
57322
  }),
57112
57323
  ...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
@@ -57159,15 +57370,26 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
57159
57370
  * is the group-level twin of `matcher`: importing the group without it would
57160
57371
  * widen every hook in it, so the group is skipped instead.
57161
57372
  */
57162
- function describeGroupSkipReason({ rawEntry, converterConfig }) {
57373
+ function describeGroupSkipReason({ rawEntry, eventName, converterConfig }) {
57163
57374
  const entry = rawEntry;
57164
- for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
57375
+ for (const { tool, valueType, subdividesGroup, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
57165
57376
  const value = entry[tool];
57166
- if (subdividesGroup !== true || value === void 0) continue;
57377
+ if (subdividesGroup !== true || onInvalidImport === "drop-field" || value === void 0 || !isGroupFieldOnEvent(events, eventName)) continue;
57167
57378
  if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${quoteValueForWarning(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
57168
57379
  }
57169
57380
  }
57170
57381
  /**
57382
+ * Warn about each `"drop-field"` group value that is present but unusable;
57383
+ * {@link importGroupPassthroughFields} leaves it off the imported hooks.
57384
+ */
57385
+ function warnDroppedGroupFields({ rawEntry, eventName, converterConfig, warn }) {
57386
+ const entry = rawEntry;
57387
+ for (const { tool, valueType, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
57388
+ const value = entry[tool];
57389
+ if (onInvalidImport === "drop-field" && value !== void 0 && isGroupFieldOnEvent(events, eventName) && !isGroupPassthroughValue(value, valueType)) warn?.(`Dropping the unusable "${tool}" (${quoteValueForWarning(value)}) of a "${eventName}" matcher group while importing; its hooks are imported without it.`);
57390
+ }
57391
+ }
57392
+ /**
57171
57393
  * Why this hook cannot be imported, or `undefined` when it can.
57172
57394
  */
57173
57395
  function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
@@ -57194,12 +57416,19 @@ function toolMatcherEntryToCanonical({ rawEntry, eventName, converterConfig, war
57194
57416
  const hookDefs = rawEntry.hooks ?? [];
57195
57417
  const groupSkipReason = describeGroupSkipReason({
57196
57418
  rawEntry,
57419
+ eventName,
57197
57420
  converterConfig
57198
57421
  });
57199
57422
  if (groupSkipReason !== void 0) {
57200
57423
  warn?.(groupSkipReason);
57201
57424
  return [];
57202
57425
  }
57426
+ warnDroppedGroupFields({
57427
+ rawEntry,
57428
+ eventName,
57429
+ converterConfig,
57430
+ warn
57431
+ });
57203
57432
  const definitions = [];
57204
57433
  for (const h of hookDefs) {
57205
57434
  const skipReason = describeHookSkipReason({
@@ -60538,6 +60767,23 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
60538
60767
  });
60539
60768
  }
60540
60769
  };
60770
+ // src/features/hooks/hook-matcher-tool-names.ts
60771
+ const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60772
+ function isMatcherName(token) {
60773
+ return !token.startsWith("\\") && !token.startsWith("[");
60774
+ }
60775
+ /**
60776
+ * Rewrites the whole tool names inside a hook `matcher` regex through `names`,
60777
+ * leaving escapes, character classes and unknown names as written. Used by
60778
+ * adapters whose runtime tool names differ from the canonical (Claude Code)
60779
+ * spelling.
60780
+ */
60781
+ function replaceMatcherNames(matcher, names) {
60782
+ return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60783
+ }
60784
+ function invertToolNames(names) {
60785
+ return Object.fromEntries(Object.entries(names).map(([canonical, tool]) => [tool, canonical]));
60786
+ }
60541
60787
  // src/features/hooks/cortexcode-hooks.ts
60542
60788
  const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
60543
60789
  const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
@@ -60548,14 +60794,7 @@ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
60548
60794
  Grep: "grep",
60549
60795
  Glob: "glob"
60550
60796
  };
60551
- const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_TOOL_NAMES).map(([canonical, tool]) => [tool, canonical]));
60552
- const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60553
- function isMatcherName(token) {
60554
- return !token.startsWith("\\") && !token.startsWith("[");
60555
- }
60556
- function replaceMatcherNames(matcher, names) {
60557
- return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60558
- }
60797
+ const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60559
60798
  function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
60560
60799
  const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60561
60800
  const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
@@ -65716,6 +65955,61 @@ var TabnineHooks = class TabnineHooks extends ToolHooks {
65716
65955
  });
65717
65956
  }
65718
65957
  };
65958
+ // src/features/hooks/trae-hooks.ts
65959
+ const TRAE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
65960
+ "sessionStart",
65961
+ "beforeSubmitPrompt",
65962
+ "stop"
65963
+ ]);
65964
+ const CANONICAL_TO_TRAE_TOOL_NAMES = { Bash: "RunCommand" };
65965
+ const TRAE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_TRAE_TOOL_NAMES);
65966
+ const TRAE_SPEC = {
65967
+ displayName: "TRAE",
65968
+ overrideKey: "trae",
65969
+ converterConfig: {
65970
+ supportedEvents: TRAE_HOOK_EVENTS,
65971
+ canonicalToToolEventNames: CANONICAL_TO_TRAE_EVENT_NAMES,
65972
+ toolToCanonicalEventNames: TRAE_TO_CANONICAL_EVENT_NAMES,
65973
+ projectDirVar: "$TRAE_PROJECT_DIR",
65974
+ stripsProjectDirVarOnImportOnly: true,
65975
+ importedProjectDirVarAliases: ["$CLAUDE_PROJECT_DIR"],
65976
+ noMatcherEvents: TRAE_NO_MATCHER_EVENTS,
65977
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
65978
+ groupPassthroughFields: [{
65979
+ canonical: "loop_limit",
65980
+ tool: "loop_limit",
65981
+ valueType: "number",
65982
+ events: /* @__PURE__ */ new Set(["stop"]),
65983
+ subdividesGroup: true,
65984
+ onInvalidImport: "drop-field"
65985
+ }],
65986
+ matcherTranslation: {
65987
+ toTool: ({ matcher }) => replaceMatcherNames(matcher, CANONICAL_TO_TRAE_TOOL_NAMES),
65988
+ toCanonical: (matcher) => replaceMatcherNames(matcher, TRAE_TO_CANONICAL_TOOL_NAMES)
65989
+ }
65990
+ }
65991
+ };
65992
+ /**
65993
+ * TRAE hooks.
65994
+ *
65995
+ * Hooks live under the top-level `hooks` key of the dedicated `.trae/hooks.json`
65996
+ * (project) and `~/.trae/hooks.json` (user scope). The IDE also writes a
65997
+ * `version` key there, so generation merges the `hooks` key into the file (see
65998
+ * `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
65999
+ *
66000
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
66001
+ */
66002
+ var TraeHooks = class extends SettingsJsonHooks {
66003
+ static getSpec() {
66004
+ return TRAE_SPEC;
66005
+ }
66006
+ static getSettablePaths(_options = {}) {
66007
+ return {
66008
+ relativeDirPath: TRAE_DIR,
66009
+ relativeFilePath: TRAE_HOOKS_FILE_NAME
66010
+ };
66011
+ }
66012
+ };
65719
66013
  // src/features/hooks/vibe-hooks.ts
65720
66014
  const VIBE_DIR = ".vibe";
65721
66015
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -66900,6 +67194,21 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
66900
67194
  supportedHookTypes: ["command"],
66901
67195
  supportsMatcher: true
66902
67196
  }],
67197
+ ["trae", {
67198
+ class: TraeHooks,
67199
+ meta: {
67200
+ supportsProject: true,
67201
+ supportsGlobal: true,
67202
+ supportsImport: true
67203
+ },
67204
+ ...converterCapabilities(TraeHooks.getConverterConfig()),
67205
+ supportsMatcher: true,
67206
+ matcherEvents: [
67207
+ "preToolUse",
67208
+ "postToolUse",
67209
+ "notification"
67210
+ ]
67211
+ }],
66903
67212
  ["vibe", {
66904
67213
  class: VibeHooks,
66905
67214
  meta: {
@@ -69008,69 +69317,6 @@ var AiassistantMcp = class extends ToolMcp {
69008
69317
  });
69009
69318
  }
69010
69319
  };
69011
- // src/features/shared/amp-settings.ts
69012
- /**
69013
- * Parse an Amp `settings.json` / `settings.jsonc` document.
69014
- *
69015
- * Shared by the MCP and permissions adapters because both read the very same
69016
- * file. Two separately maintained copies drifted once already: the permissions
69017
- * side was hardened while the MCP side kept calling `jsonc-parser` directly,
69018
- * which left it importing MCP servers reachable only through an injected
69019
- * `__proto__`.
69020
- *
69021
- * {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
69022
- * error instead of returning `jsonc-parser`'s best-effort value, and it
69023
- * rebuilds every object from its own enumerable entries, so a root `__proto__`
69024
- * cannot swap the returned object's prototype and `constructor` / `prototype`
69025
- * never survive as own keys, at any depth.
69026
- *
69027
- * Removal is silent, matching every other tool-side adapter (opencode, kilo,
69028
- * copilot). Reporting the removed keys through `droppedPollutionKeysError` is
69029
- * reserved for the three files a user authors under `.rulesync/`, whose whole
69030
- * purpose is to be turned into tool config — a key that vanishes there needs
69031
- * explaining, whereas here the surrounding settings are the user's own file
69032
- * and are left alone.
69033
- *
69034
- * Note the consequence for a root `__proto__`: sanitizing runs before the
69035
- * plain-object check, so such a document parses successfully with the key
69036
- * stripped rather than failing that check. That is deliberate — dropping the
69037
- * one poisoned key keeps the user's unrelated settings — and matches the
69038
- * sanitize-before-the-root-check design the shared config gateway documents.
69039
- */
69040
- function parseAmpSettings({ fileContent }) {
69041
- let parsed;
69042
- try {
69043
- parsed = parseJsonc$6(fileContent || "{}");
69044
- } catch (error) {
69045
- throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
69046
- }
69047
- if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
69048
- return parsed;
69049
- }
69050
- /**
69051
- * Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
69052
- * an existing user file is read-modified-written in place instead of a fresh
69053
- * `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
69054
- * `settings.json` when neither file exists: Amp's canonically documented
69055
- * settings file (`.amp/settings.json`), shared by the MCP and permissions
69056
- * adapters, so generating both on a clean repo writes a single settings file.
69057
- */
69058
- async function resolveAmpSettingsFile(jsonDir) {
69059
- const jsoncContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
69060
- if (jsoncContent !== null) return {
69061
- fileContent: jsoncContent,
69062
- relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
69063
- };
69064
- const jsonContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_FILE_NAME));
69065
- if (jsonContent !== null) return {
69066
- fileContent: jsonContent,
69067
- relativeFilePath: AMP_SETTINGS_FILE_NAME
69068
- };
69069
- return {
69070
- fileContent: null,
69071
- relativeFilePath: AMP_SETTINGS_FILE_NAME
69072
- };
69073
- }
69074
69320
  // src/features/mcp/amp-mcp.ts
69075
69321
  const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
69076
69322
  function filterMcpServers(mcpServers) {
@@ -72056,7 +72302,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
72056
72302
  fileKey: sharedConfigFileKey(paths),
72057
72303
  feature: "mcp",
72058
72304
  existingContent,
72059
- patch: { servers: convertToCopilotFormat(mcpServers) },
72305
+ patch: { servers: undefinedWhenEmpty(convertToCopilotFormat(mcpServers)) },
72060
72306
  filePath,
72061
72307
  logger
72062
72308
  }),
@@ -74377,14 +74623,20 @@ function resolveGooseUrl(config) {
74377
74623
  }
74378
74624
  /**
74379
74625
  * Determines the Goose `type` for a server based on its command/url/transport.
74626
+ *
74627
+ * Goose no longer has an SSE extension type (it skips a `type: sse` entry as
74628
+ * unsupported), so every remote server is written as `streamable_http`.
74380
74629
  */
74381
74630
  function resolveGooseType(config, url) {
74382
74631
  if (config.command !== void 0) return "stdio";
74383
- if (url !== void 0) return canonicalTransport(config) === "sse" ? "sse" : "streamable_http";
74632
+ if (url !== void 0) return "streamable_http";
74384
74633
  return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
74385
74634
  }
74386
74635
  /**
74387
- * The Goose extension types that carry an MCP server. Goose also documents
74636
+ * The Goose extension types that carry an MCP server. `sse` is no longer one
74637
+ * Goose reads, but it stays here so a stale `type: sse` entry is still
74638
+ * recognized: imported as canonical `sse` and replaced by the regenerated
74639
+ * same-name `streamable_http` entry. Goose also documents
74388
74640
  * `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
74389
74641
  * no canonical MCP counterpart: they name capabilities Goose provides itself
74390
74642
  * rather than a server rulesync could describe.
@@ -74444,7 +74696,8 @@ function convertServerToGooseExtension(name, config, logger) {
74444
74696
  warnWithFallback(logger, `Goose extension "${name}" has no command to run; skipping it rather than writing a stdio extension Goose cannot start to ~/.config/goose/config.yaml.`);
74445
74697
  return;
74446
74698
  }
74447
- } else if (gooseType === "sse" || gooseType === "streamable_http") {
74699
+ } else if (gooseType === "streamable_http") {
74700
+ if (canonicalTransport(config) === "sse") warnWithFallback(logger, `Goose extension "${name}" uses SSE, which Goose no longer supports; writing it as streamable_http, which only works if the server also speaks Streamable HTTP.`);
74448
74701
  if (url !== void 0) ext.uri = url;
74449
74702
  if (isPlainObject(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
74450
74703
  }
@@ -74457,8 +74710,9 @@ function convertServerToGooseExtension(name, config, logger) {
74457
74710
  * Converts rulesync canonical MCP servers into Goose `extensions:` entries.
74458
74711
  *
74459
74712
  * Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
74460
- * | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
74461
- * remote, plus `enabled` and `timeout`.
74713
+ * | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for remote, plus
74714
+ * `enabled` and `timeout`. A canonical `sse` server is written as
74715
+ * `streamable_http` with a warning, since Goose dropped SSE support.
74462
74716
  *
74463
74717
  * `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
74464
74718
  * holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
@@ -74589,7 +74843,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
74589
74843
  * `~/.config/goose/config.yaml`. That file also holds other Goose settings
74590
74844
  * (model, provider, ...), so generation merges the `extensions:` block into the
74591
74845
  * existing config instead of overwriting it, and the file is never deleted.
74592
- * This location supports both stdio and remote (http/sse) servers.
74846
+ * This location supports both stdio and remote servers (SSE is written as
74847
+ * `streamable_http`, since Goose no longer reads `type: sse`).
74593
74848
  * - **Project**: a stdio-only open-plugin manifest at
74594
74849
  * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
74595
74850
  * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
@@ -79307,6 +79562,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
79307
79562
  const managedNames = new Set(Object.keys(rulesyncServers));
79308
79563
  const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
79309
79564
  const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
79565
+ const servers = [...mcpServers, ...unmanagedServers];
79310
79566
  return new VibeMcp({
79311
79567
  outputRoot,
79312
79568
  relativeDirPath: paths.relativeDirPath,
@@ -79315,7 +79571,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
79315
79571
  fileKey: sharedConfigFileKey(paths),
79316
79572
  feature: "mcp",
79317
79573
  existingContent,
79318
- patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
79574
+ patch: { mcp_servers: servers.length > 0 ? servers : void 0 },
79319
79575
  filePath
79320
79576
  }),
79321
79577
  validate,
@@ -82739,6 +82995,20 @@ function isUnicodeClass(rest) {
82739
82995
  }));
82740
82996
  }
82741
82997
  /**
82998
+ * Return a function that, for a `[:` at `index`, gives the position of the
82999
+ * next `:]` (Go reads everything between as the class name), or -1. The
83000
+ * position is cached and `index` only grows, so a long run of `[:` with no
83001
+ * `:]` costs linear time rather than a scan to the end at each one.
83002
+ */
83003
+ function posixEndFinder(source) {
83004
+ let next = -2;
83005
+ return (index) => {
83006
+ if (!source.startsWith("[:", index)) return -1;
83007
+ if (next !== -1 && next < index + 2) next = source.indexOf(":]", index + 2);
83008
+ return next;
83009
+ };
83010
+ }
83011
+ /**
82742
83012
  * Replace each POSIX class (`[:alpha:]`) inside a bracket class with a plain
82743
83013
  * character, so JavaScript can compile the rest, or return `undefined` for an
82744
83014
  * unknown one. Only inside a bracket is `[:name:]` a POSIX class; a bare
@@ -82748,6 +83018,7 @@ function replacePosixClasses(source) {
82748
83018
  let result = "";
82749
83019
  let inClass = false;
82750
83020
  let index = 0;
83021
+ const findPosixEnd = posixEndFinder(source);
82751
83022
  while (index < source.length) {
82752
83023
  const character = source[index] ?? "";
82753
83024
  if (character === "\\") {
@@ -82772,11 +83043,11 @@ function replacePosixClasses(source) {
82772
83043
  }
82773
83044
  continue;
82774
83045
  }
82775
- const posix = /^\[:\^?([\s\S]*?):\]/.exec(source.slice(index));
82776
- if (posix !== null) {
82777
- if (!POSIX_CLASSES.has(posix[1] ?? "")) return;
83046
+ const posixEnd = findPosixEnd(index);
83047
+ if (posixEnd !== -1) {
83048
+ if (!POSIX_CLASSES.has(source.slice(index + 2, posixEnd).replace(/^\^/, ""))) return;
82778
83049
  result += "a";
82779
- index += posix[0].length;
83050
+ index = posixEnd + 2;
82780
83051
  continue;
82781
83052
  }
82782
83053
  if (character === "]") inClass = false;
@@ -82855,6 +83126,7 @@ function hasClassEscapeRangeEnd(source) {
82855
83126
  let inClass = false;
82856
83127
  let afterCharacter = false;
82857
83128
  let index = 0;
83129
+ const findPosixEnd = posixEndFinder(source);
82858
83130
  while (index < source.length) {
82859
83131
  const character = source[index] ?? "";
82860
83132
  if (character === "\\") {
@@ -82870,7 +83142,7 @@ function hasClassEscapeRangeEnd(source) {
82870
83142
  });
82871
83143
  continue;
82872
83144
  }
82873
- const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
83145
+ const posixEnd = findPosixEnd(index);
82874
83146
  if (character === "]" || posixEnd !== -1) {
82875
83147
  inClass = character !== "]";
82876
83148
  afterCharacter = false;
@@ -83432,6 +83704,8 @@ function buildToolPatternEntry({ toolName, pattern }) {
83432
83704
  * - `execute_url` takes a domain, like `read_url`.
83433
83705
  * - `command` and `unsandboxed` take literal words or a per-word `regex:`,
83434
83706
  * matched word by word (see `antigravity-command-patterns.ts`).
83707
+ * - `mcp` takes `server/tool` or `server/*`, matched exactly. A canonical
83708
+ * `mcp__<server>__<tool>` category is written as that target.
83435
83709
  *
83436
83710
  * A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
83437
83711
  * matches nothing, so a deny written that way blocks nothing. A pattern with
@@ -83455,7 +83729,8 @@ const DENY_HINTS = {
83455
83729
  read_file: "deny a directory or file path instead",
83456
83730
  write_file: "deny a directory or file path instead",
83457
83731
  command: "deny the first words of the command instead",
83458
- unsandboxed: "deny the first words of the command instead"
83732
+ unsandboxed: "deny the first words of the command instead",
83733
+ mcp: "deny the whole MCP server or tool instead"
83459
83734
  };
83460
83735
  function hasGlob(pattern) {
83461
83736
  return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
@@ -83511,6 +83786,40 @@ function toTarget(action, pattern) {
83511
83786
  if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
83512
83787
  return { target: pattern };
83513
83788
  }
83789
+ const MCP_CATEGORY_PREFIX = "mcp__";
83790
+ const MCP_TARGET_SEPARATORS = /[/()]/;
83791
+ /**
83792
+ * The `mcp` target of a canonical `mcp__<server>__<tool>` category:
83793
+ * `<server>/<tool>`, `<server>/*` for a whole server (`mcp__<server>`,
83794
+ * `mcp__<server>__*`), or `*` for every MCP tool (`mcp__*`, `mcp__*__*`). The
83795
+ * server ends at the first `__`, as elsewhere in rulesync. Antigravity matches
83796
+ * names exactly, so a glob, an empty name, or a `/` or parenthesis in a name
83797
+ * has no target; when only the tool part is at fault, its server is the `lead`
83798
+ * a deny widens to.
83799
+ */
83800
+ function toMcpTarget(category) {
83801
+ const remainder = category.slice(5);
83802
+ const separator = remainder.indexOf("__");
83803
+ const server = separator === -1 ? remainder : remainder.slice(0, separator);
83804
+ const tool = separator === -1 ? "*" : remainder.slice(separator + 2);
83805
+ if (server === "*" && tool === "*") return { target: "*" };
83806
+ const skipReason = "Antigravity MCP targets name one server or one tool exactly. Only `mcp__<server>`, `mcp__<server>__*`, `mcp__<server>__<tool>` or `mcp__*` can be expressed";
83807
+ if (server.length === 0 || MCP_TARGET_SEPARATORS.test(server) || hasGlob(server)) return { skipReason };
83808
+ if (tool === "*") return { target: `${server}/*` };
83809
+ return tool.length === 0 || MCP_TARGET_SEPARATORS.test(tool) || hasGlob(tool) ? {
83810
+ skipReason,
83811
+ lead: `${server}/*`
83812
+ } : { target: `${server}/${tool}` };
83813
+ }
83814
+ /** The target of a canonical `mcp__...` rule, with the `lead` a deny widens to. */
83815
+ function toMcpRuleTarget(category, pattern) {
83816
+ const target = toMcpTarget(category);
83817
+ if (pattern === "*" || !("target" in target)) return target;
83818
+ return {
83819
+ skipReason: "Antigravity MCP targets name a server or a tool, not the tool's arguments",
83820
+ lead: target.target
83821
+ };
83822
+ }
83514
83823
  /**
83515
83824
  * The leading directories of a path glob (`src` for `src/*.ts`), or
83516
83825
  * `undefined` when it has none. A directory target covers everything inside
@@ -83533,6 +83842,20 @@ function toWidenedDenyTarget(action, pattern) {
83533
83842
  if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
83534
83843
  if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
83535
83844
  }
83845
+ /** The entry a deny is widened to, and what it now blocks. */
83846
+ function describeWidenedDeny({ action, prefix, isMcp }) {
83847
+ if (isMcp) return prefix === "*" ? {
83848
+ entry: action,
83849
+ scope: "every MCP tool"
83850
+ } : {
83851
+ entry: `${action}(${prefix})`,
83852
+ scope: `every call of \`${action}(${prefix})\``
83853
+ };
83854
+ return {
83855
+ entry: `${action}(${prefix})`,
83856
+ scope: PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``
83857
+ };
83858
+ }
83536
83859
  /**
83537
83860
  * Warn about a path target that is a home or variable reference. Antigravity
83538
83861
  * documents absolute and workspace-relative paths only, so such a reference
@@ -83554,14 +83877,20 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
83554
83877
  * nothing enforces it, and a widened one is warned about too, because it
83555
83878
  * blocks more than it says.
83556
83879
  */
83557
- function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
83558
- const result = pattern === "*" ? { target: "*" } : toTarget(action, pattern);
83880
+ function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
83881
+ const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
83882
+ const action = isMcp ? "mcp" : toolAction;
83883
+ const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
83559
83884
  const rule = `${category}: { "${pattern}": "${decision}" }`;
83560
83885
  if ("skipReason" in result) {
83561
- const prefix = decision === "deny" ? toWidenedDenyTarget(action, pattern) : void 0;
83886
+ const prefix = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
83562
83887
  if (prefix !== void 0) {
83563
- const scope = PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``;
83564
- logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
83888
+ const { entry, scope } = describeWidenedDeny({
83889
+ action,
83890
+ prefix,
83891
+ isMcp
83892
+ });
83893
+ logger.warn(`${toolLabel} permissions: widening ${rule} to ${entry}. ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
83565
83894
  warnHomeOrVariablePath({
83566
83895
  action,
83567
83896
  target: prefix,
@@ -83570,7 +83899,7 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
83570
83899
  logger,
83571
83900
  toolLabel
83572
83901
  });
83573
- return `${action}(${prefix})`;
83902
+ return entry;
83574
83903
  }
83575
83904
  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`;
83576
83905
  logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
@@ -83714,6 +84043,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
83714
84043
  mcp: "mcp"
83715
84044
  };
83716
84045
  function toAntigravityCliToolName(canonical) {
84046
+ if (canonical.startsWith("mcp__")) return "mcp";
83717
84047
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
83718
84048
  }
83719
84049
  function toCanonicalToolName$9(cliName) {
@@ -83776,10 +84106,11 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
83776
84106
  logger
83777
84107
  });
83778
84108
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
84109
+ const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
83779
84110
  const existingPermissions = settings.permissions ?? {};
83780
- const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83781
- const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83782
- const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
84111
+ const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
84112
+ const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
84113
+ const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
83783
84114
  const mergedPermissions = { ...existingPermissions };
83784
84115
  const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
83785
84116
  const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
@@ -83888,6 +84219,7 @@ const IDE_ACTION_TO_CANONICAL = {
83888
84219
  mcp: "mcp"
83889
84220
  };
83890
84221
  function toIdeAction(canonical) {
84222
+ if (canonical.startsWith("mcp__")) return "mcp";
83891
84223
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
83892
84224
  }
83893
84225
  function toCanonicalCategory$2(ideAction) {
@@ -83944,8 +84276,9 @@ var AntigravityIdePermissions = class AntigravityIdePermissions extends ToolPerm
83944
84276
  logger
83945
84277
  });
83946
84278
  const managedActions = new Set(Object.keys(config.permission).map((category) => toIdeAction(category)));
84279
+ const isManagedAction = (action) => managedActions.has(action) || managedActions.has("mcp") && action.startsWith("mcp__");
83947
84280
  const existingPermissions = settings.permissions ?? {};
83948
- const preserve = (entries) => (entries ?? []).filter((entry) => !managedActions.has(parseToolPatternEntry(entry).toolName));
84281
+ const preserve = (entries) => (entries ?? []).filter((entry) => !isManagedAction(parseToolPatternEntry(entry).toolName));
83949
84282
  const mergedPermissions = { ...existingPermissions };
83950
84283
  const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
83951
84284
  const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());