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.
@@ -873,6 +873,7 @@ const hooksProcessorToolTargetTuple = [
873
873
  "qwencode",
874
874
  "reasonix",
875
875
  "tabnine",
876
+ "trae",
876
877
  "grokcli",
877
878
  "gitlabduo",
878
879
  "lettacode",
@@ -3923,6 +3924,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3923
3924
  ".poolside/settings.yaml",
3924
3925
  ".config/poolside/settings.yaml",
3925
3926
  ".tabnine/agent/settings.json",
3927
+ ".trae/hooks.json",
3926
3928
  ".vibe/config.toml",
3927
3929
  ".zcode/config.json",
3928
3930
  ".zcode/cli/config.json",
@@ -6005,6 +6007,34 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
6005
6007
  };
6006
6008
  const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CORTEXCODE_EVENT_NAMES);
6007
6009
  /**
6010
+ * Hook events supported by TRAE.
6011
+ *
6012
+ * TRAE reads `.trae/hooks.json` (project) and `~/.trae/hooks.json` (user):
6013
+ * `{ "version"?: 1, "hooks": { "<Event>": [{ "matcher"?, "hooks": [{ "type":
6014
+ * "command", "command", "timeout"? }] }] } }` with Claude-Code-style
6015
+ * PascalCase event names. `$TRAE_PROJECT_DIR` resolves to the workspace root
6016
+ * inside commands.
6017
+ *
6018
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
6019
+ */
6020
+ const TRAE_HOOK_EVENTS = [
6021
+ "sessionStart",
6022
+ "beforeSubmitPrompt",
6023
+ "preToolUse",
6024
+ "postToolUse",
6025
+ "stop",
6026
+ "notification"
6027
+ ];
6028
+ const CANONICAL_TO_TRAE_EVENT_NAMES = {
6029
+ sessionStart: "SessionStart",
6030
+ beforeSubmitPrompt: "UserPromptSubmit",
6031
+ preToolUse: "PreToolUse",
6032
+ postToolUse: "PostToolUse",
6033
+ stop: "Stop",
6034
+ notification: "Notification"
6035
+ };
6036
+ const TRAE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_TRAE_EVENT_NAMES);
6037
+ /**
6008
6038
  * Hook events supported by CodeBuddy Code.
6009
6039
  *
6010
6040
  * CodeBuddy Code reads hooks from the `hooks` key of `.codebuddy/settings.json`
@@ -6477,6 +6507,7 @@ const HooksConfigSchema = z.looseObject({
6477
6507
  bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6478
6508
  codebuddy: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6479
6509
  cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6510
+ trae: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6480
6511
  commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6481
6512
  lettacode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6482
6513
  continue: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
@@ -14159,6 +14190,14 @@ const SHARED_CONFIG_OWNERSHIP = {
14159
14190
  }
14160
14191
  }
14161
14192
  },
14193
+ ".trae/hooks.json": {
14194
+ format: "json",
14195
+ invalidRootPolicy: "error",
14196
+ features: { hooks: {
14197
+ kind: "replace-owned-keys",
14198
+ ownedKeys: ["hooks"]
14199
+ } }
14200
+ },
14162
14201
  ".trae/permission/global.json": {
14163
14202
  format: "json",
14164
14203
  invalidRootPolicy: "error",
@@ -23284,10 +23323,13 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23284
23323
  * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23285
23324
  * @see https://docs.trae.ai/ide/ignore-files?_lang=en
23286
23325
  * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
23326
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
23287
23327
  */
23288
23328
  const TRAE_DIR = ".trae";
23289
23329
  /** Project MCP servers (`mcpServers` object), project scope only. */
23290
23330
  const TRAE_MCP_FILE_NAME = "mcp.json";
23331
+ /** Hooks: `.trae/hooks.json` (project) and `~/.trae/hooks.json` (global). */
23332
+ const TRAE_HOOKS_FILE_NAME = "hooks.json";
23291
23333
  /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
23292
23334
  const TRAE_SKILLS_DIR_PATH = join(TRAE_DIR, "skills");
23293
23335
  /**
@@ -25212,12 +25254,80 @@ var AiassistantSkill = class AiassistantSkill extends ToolSkill {
25212
25254
  });
25213
25255
  }
25214
25256
  };
25257
+ // src/features/shared/amp-settings.ts
25258
+ /**
25259
+ * Parse an Amp `settings.json` / `settings.jsonc` document.
25260
+ *
25261
+ * Shared by the MCP and permissions adapters because both read the very same
25262
+ * file. Two separately maintained copies drifted once already: the permissions
25263
+ * side was hardened while the MCP side kept calling `jsonc-parser` directly,
25264
+ * which left it importing MCP servers reachable only through an injected
25265
+ * `__proto__`.
25266
+ *
25267
+ * {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
25268
+ * error instead of returning `jsonc-parser`'s best-effort value, and it
25269
+ * rebuilds every object from its own enumerable entries, so a root `__proto__`
25270
+ * cannot swap the returned object's prototype and `constructor` / `prototype`
25271
+ * never survive as own keys, at any depth.
25272
+ *
25273
+ * Removal is silent, matching every other tool-side adapter (opencode, kilo,
25274
+ * copilot). Reporting the removed keys through `droppedPollutionKeysError` is
25275
+ * reserved for the three files a user authors under `.rulesync/`, whose whole
25276
+ * purpose is to be turned into tool config — a key that vanishes there needs
25277
+ * explaining, whereas here the surrounding settings are the user's own file
25278
+ * and are left alone.
25279
+ *
25280
+ * Note the consequence for a root `__proto__`: sanitizing runs before the
25281
+ * plain-object check, so such a document parses successfully with the key
25282
+ * stripped rather than failing that check. That is deliberate — dropping the
25283
+ * one poisoned key keeps the user's unrelated settings — and matches the
25284
+ * sanitize-before-the-root-check design the shared config gateway documents.
25285
+ */
25286
+ function parseAmpSettings({ fileContent }) {
25287
+ let parsed;
25288
+ try {
25289
+ parsed = parseJsonc(fileContent || "{}");
25290
+ } catch (error) {
25291
+ throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
25292
+ }
25293
+ if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
25294
+ return parsed;
25295
+ }
25296
+ /**
25297
+ * Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
25298
+ * an existing user file is read-modified-written in place instead of a fresh
25299
+ * `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
25300
+ * `settings.json` when neither file exists: Amp's canonically documented
25301
+ * settings file (`.amp/settings.json`), shared by the MCP and permissions
25302
+ * adapters, so generating both on a clean repo writes a single settings file.
25303
+ */
25304
+ async function resolveAmpSettingsFile(jsonDir) {
25305
+ const jsoncContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
25306
+ if (jsoncContent !== null) return {
25307
+ fileContent: jsoncContent,
25308
+ relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
25309
+ };
25310
+ const jsonContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_FILE_NAME));
25311
+ if (jsonContent !== null) return {
25312
+ fileContent: jsonContent,
25313
+ relativeFilePath: AMP_SETTINGS_FILE_NAME
25314
+ };
25315
+ return {
25316
+ fileContent: null,
25317
+ relativeFilePath: AMP_SETTINGS_FILE_NAME
25318
+ };
25319
+ }
25215
25320
  // src/features/skills/amp-skill.ts
25216
25321
  const AmpSkillFrontmatterSchema = z.looseObject({
25217
25322
  name: z.string(),
25218
25323
  description: z.string()
25219
25324
  });
25220
25325
  /**
25326
+ * Amp setting that makes it skip `~/.config/agents/skills/` and
25327
+ * `~/.agents/skills/`. https://ampcode.com/docs/cli/settings
25328
+ */
25329
+ const AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY = "amp.skills.disableGlobalAgentsSkills";
25330
+ /**
25221
25331
  * Represents an Amp Agent Skill directory.
25222
25332
  *
25223
25333
  * Amp natively reads Agent Skills following the open standard: a directory with a
@@ -25252,6 +25362,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
25252
25362
  ...global && { importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH, AMP_SKILLS_NATIVE_GLOBAL_DIR] }
25253
25363
  };
25254
25364
  }
25365
+ /**
25366
+ * Warn when the user's Amp settings disable the global agent-agnostic skill
25367
+ * roots, because the global skills rulesync writes to
25368
+ * `~/.config/agents/skills/` would then never load. Only the user settings
25369
+ * file is checked: a workspace `.amp/settings.json(c)` depends on where Amp
25370
+ * is started, which global generation cannot know.
25371
+ */
25372
+ static async getGenerationWarnings({ outputRoot, global, logger }) {
25373
+ if (!global) return [];
25374
+ const { fileContent, relativeFilePath } = await resolveAmpSettingsFile(join(outputRoot, AMP_GLOBAL_DIR));
25375
+ if (fileContent === null) return [];
25376
+ let settings;
25377
+ try {
25378
+ settings = parseAmpSettings({ fileContent });
25379
+ } catch (error) {
25380
+ logger.debug(`Skipping the Amp skills settings check for ${join(AMP_GLOBAL_DIR, relativeFilePath)}: ${formatError(error)}`);
25381
+ return [];
25382
+ }
25383
+ if (settings[AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY] !== true) return [];
25384
+ return [`Amp will not load the global skills rulesync writes to ~/${AMP_SKILLS_GLOBAL_DIR}/: '${AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY}' is true in ~/${join(AMP_GLOBAL_DIR, relativeFilePath)}.`];
25385
+ }
25255
25386
  getFrontmatter() {
25256
25387
  return AmpSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
25257
25388
  }
@@ -33228,7 +33359,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
33228
33359
  async convertRulesyncDirsToToolDirs(rulesyncDirs) {
33229
33360
  const rulesyncSkills = rulesyncDirs.filter((dir) => dir instanceof RulesyncSkill);
33230
33361
  const factory = this.getFactory(this.toolTarget);
33231
- return (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
33362
+ const toolSkills = (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
33232
33363
  const isClaudecodeScheduledTask = rulesyncSkill.getFrontmatter().claudecode?.["scheduled-task"] === true;
33233
33364
  if (isClaudecodeScheduledTask && this.toolTarget !== "claudecode" && this.toolTarget !== "claudecode-legacy") return null;
33234
33365
  if (!factory.class.isTargetedByRulesyncSkill(rulesyncSkill)) return null;
@@ -33255,6 +33386,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
33255
33386
  logger: this.logger
33256
33387
  });
33257
33388
  }))).filter((skill) => skill !== null);
33389
+ if (toolSkills.length > 0 && factory.class.getGenerationWarnings) {
33390
+ const warnings = await factory.class.getGenerationWarnings({
33391
+ outputRoot: this.outputRoot,
33392
+ global: this.global,
33393
+ logger: this.logger
33394
+ });
33395
+ for (const warning of warnings) this.logger.warn(warning);
33396
+ }
33397
+ return toolSkills;
33258
33398
  }
33259
33399
  async convertToolDirsToRulesyncDirs(toolDirs) {
33260
33400
  const toolSkills = toolDirs.filter((dir) => dir instanceof ToolSkill);
@@ -47460,6 +47600,45 @@ var DshRule = class DshRule extends NestedAgentsmdRule {
47460
47600
  };
47461
47601
  }
47462
47602
  };
47603
+ // src/features/rules/output-style-paths.ts
47604
+ /**
47605
+ * Shared helpers for tool rule adapters that write opted-in rules as custom
47606
+ * output styles (`<tool dir>/output-styles/<name>.md`), e.g. Qoder and
47607
+ * Factory Droid. Both tools read only the direct `.md` children of their
47608
+ * output-styles directory, with optional `name` / `description` frontmatter.
47609
+ */
47610
+ /**
47611
+ * Whether the given path is a direct `.md` child of `outputStylesDirPath`.
47612
+ */
47613
+ function isDirectMarkdownChild({ outputStylesDirPath, relativeDirPath, relativeFilePath }) {
47614
+ return relativeDirPath === outputStylesDirPath && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
47615
+ }
47616
+ /**
47617
+ * Whether a rule opts into the `output-style` channel: only a non-root rule
47618
+ * can, since an output style is a file of its own.
47619
+ */
47620
+ function isOutputStyleChannel({ root, channel }) {
47621
+ return !root && channel === "output-style";
47622
+ }
47623
+ /**
47624
+ * Return the file stem of an output-style source rule, throwing `message`
47625
+ * when the rule is nested in a subdirectory of the rules directory: the
47626
+ * output file is named after the rule, and the tools read only the top level
47627
+ * of their output-styles directory.
47628
+ */
47629
+ function resolveTopLevelOutputStyleStem({ sourcePath, message }) {
47630
+ if (basename(sourcePath) !== sourcePath) throw new Error(message);
47631
+ return basename(sourcePath, extname(sourcePath));
47632
+ }
47633
+ /**
47634
+ * The output style's frontmatter: only the `name` and `description` that are set.
47635
+ */
47636
+ function buildOutputStyleFrontmatter({ name, description }) {
47637
+ return {
47638
+ ...name !== void 0 && { name },
47639
+ ...description !== void 0 && { description }
47640
+ };
47641
+ }
47463
47642
  // src/features/rules/factorydroid-rule.ts
47464
47643
  /**
47465
47644
  * The file-name glob `getExtraFixedFiles` returns for the output-styles
@@ -47564,7 +47743,11 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47564
47743
  * child of the output-styles directory, at either scope.
47565
47744
  */
47566
47745
  static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
47567
- return relativeDirPath === this.getOutputStylesDirPath() && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
47746
+ return isDirectMarkdownChild({
47747
+ outputStylesDirPath: this.getOutputStylesDirPath(),
47748
+ relativeDirPath,
47749
+ relativeFilePath
47750
+ });
47568
47751
  }
47569
47752
  /**
47570
47753
  * Which fixed-file channel, if any, owns the given output path. Matching on
@@ -47604,7 +47787,10 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47604
47787
  /** Whether a rule opts into the `output-style` channel (non-root only). */
47605
47788
  static isOutputStyleRule(rulesyncRule) {
47606
47789
  const frontmatter = rulesyncRule.getFrontmatter();
47607
- return !frontmatter.root && frontmatter.factorydroid?.channel === "output-style";
47790
+ return isOutputStyleChannel({
47791
+ root: frontmatter.root,
47792
+ channel: frontmatter.factorydroid?.channel
47793
+ });
47608
47794
  }
47609
47795
  /**
47610
47796
  * Factory Droid loads the channel files itself, so listing one in the root
@@ -47736,15 +47922,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47736
47922
  static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
47737
47923
  const frontmatter = rulesyncRule.getFrontmatter();
47738
47924
  const sourcePath = rulesyncRule.getRelativeFilePath();
47739
- if (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}.`);
47740
- const stem = basename(sourcePath, extname(sourcePath));
47925
+ const stem = resolveTopLevelOutputStyleStem({
47926
+ sourcePath,
47927
+ 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}.`
47928
+ });
47741
47929
  const name = frontmatter.factorydroid?.name;
47742
47930
  const styleName = name ?? stem;
47743
47931
  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.`);
47744
- const styleFrontmatter = {
47745
- ...name !== void 0 && { name },
47746
- ...frontmatter.description !== void 0 && { description: frontmatter.description }
47747
- };
47932
+ const styleFrontmatter = buildOutputStyleFrontmatter({
47933
+ name,
47934
+ description: frontmatter.description
47935
+ });
47748
47936
  const body = rulesyncRule.getBody();
47749
47937
  return new FactorydroidRule({
47750
47938
  outputRoot,
@@ -49380,12 +49568,19 @@ var QoderRule = class QoderRule extends ToolRule {
49380
49568
  * `.qoder/output-styles/`, at either scope.
49381
49569
  */
49382
49570
  static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
49383
- return relativeDirPath === QODER_OUTPUT_STYLES_DIR_PATH && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
49571
+ return isDirectMarkdownChild({
49572
+ outputStylesDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
49573
+ relativeDirPath,
49574
+ relativeFilePath
49575
+ });
49384
49576
  }
49385
49577
  /** Whether a rule opts into the `output-style` channel (non-root only). */
49386
49578
  static isOutputStyleRule(rulesyncRule) {
49387
49579
  const frontmatter = rulesyncRule.getFrontmatter();
49388
- return !frontmatter.root && frontmatter.qoder?.channel === "output-style";
49580
+ return isOutputStyleChannel({
49581
+ root: frontmatter.root,
49582
+ channel: frontmatter.qoder?.channel
49583
+ });
49389
49584
  }
49390
49585
  /**
49391
49586
  * The output-styles directory, enumerated for import only: it also holds
@@ -49496,16 +49691,18 @@ var QoderRule = class QoderRule extends ToolRule {
49496
49691
  static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
49497
49692
  const { description, qoder } = rulesyncRule.getFrontmatter();
49498
49693
  const sourcePath = rulesyncRule.getRelativeFilePath();
49499
- if (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}.`);
49500
- const name = qoder?.name;
49694
+ const stem = resolveTopLevelOutputStyleStem({
49695
+ sourcePath,
49696
+ 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}.`
49697
+ });
49501
49698
  return new QoderRule({
49502
49699
  outputRoot,
49503
49700
  relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
49504
- relativeFilePath: `${basename(sourcePath, extname(sourcePath))}.md`,
49505
- frontmatter: {
49506
- ...name !== void 0 && { name },
49507
- ...description !== void 0 && { description }
49508
- },
49701
+ relativeFilePath: `${stem}.md`,
49702
+ frontmatter: buildOutputStyleFrontmatter({
49703
+ name: qoder?.name,
49704
+ description
49705
+ }),
49509
49706
  body: rulesyncRule.getBody(),
49510
49707
  validate,
49511
49708
  root: false,
@@ -56183,9 +56380,9 @@ function groupDefinitionsByMatcher({ definitions, eventName, converterConfig, wa
56183
56380
  });
56184
56381
  translatedMatchers.set(normalizedMatcher, matcher);
56185
56382
  }
56186
- const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
56383
+ const key = [matcher, ...subdividingFields.map(({ canonical, valueType, events }) => {
56187
56384
  const value = def[canonical];
56188
- return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
56385
+ return isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
56189
56386
  })].join("\0");
56190
56387
  const group = byMatcher.get(key);
56191
56388
  if (group) group.defs.push(def);
@@ -56226,6 +56423,7 @@ function runsInPosixShell({ shell, converterConfig }) {
56226
56423
  * Apply the optional project directory variable prefix to a command string.
56227
56424
  */
56228
56425
  function applyCommandPrefix({ def, converterConfig }) {
56426
+ if (converterConfig.stripsProjectDirVarOnImportOnly) return def.command;
56229
56427
  const commandText = def.command;
56230
56428
  const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
56231
56429
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
@@ -56434,12 +56632,16 @@ const describeInvalidObject = ({ tool, canonical, value }) => isPlainObject(valu
56434
56632
  value
56435
56633
  })} Importing it would fail validation on the next run.` : `Dropping "${tool}" while importing a hook: it must be an object.`;
56436
56634
  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.`;
56635
+ function isGroupFieldOnEvent(events, eventName) {
56636
+ return events === void 0 || events.has(eventName);
56637
+ }
56437
56638
  /**
56438
56639
  * Check a value against the shape its field documents. A string field also
56439
56640
  * rejects control characters, matching the canonical `safeString` so an
56440
56641
  * imported value cannot fail validation on the next generate.
56441
56642
  */
56442
56643
  function isGroupPassthroughValue(value, valueType = "object") {
56644
+ if (valueType === "number") return typeof value === "number" && Number.isFinite(value);
56443
56645
  if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
56444
56646
  return isPlainObject(value);
56445
56647
  }
@@ -56449,10 +56651,14 @@ function isGroupPassthroughValue(value, valueType = "object") {
56449
56651
  */
56450
56652
  function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
56451
56653
  const emitted = {};
56452
- for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
56654
+ for (const { canonical, tool, valueType, events } of converterConfig.groupPassthroughFields ?? []) {
56453
56655
  const carried = defs.map((def) => def[canonical]);
56454
56656
  const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
56455
56657
  if (first === void 0) continue;
56658
+ if (!isGroupFieldOnEvent(events, eventName)) {
56659
+ logger?.warn(`"${canonical}" on "${eventName}" hooks is ignored — the tool supports "${tool}" only on ${[...events ?? []].map((event) => `"${event}"`).join(", ")} hooks.`);
56660
+ continue;
56661
+ }
56456
56662
  const firstStable = stableJson(first);
56457
56663
  const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
56458
56664
  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.`);
@@ -56464,9 +56670,9 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
56464
56670
  * Import the configured group-level passthrough fields onto a definition,
56465
56671
  * reversing {@link emitGroupPassthroughFields}.
56466
56672
  */
56467
- function importGroupPassthroughFields({ rawEntry, converterConfig }) {
56673
+ function importGroupPassthroughFields({ rawEntry, eventName, converterConfig }) {
56468
56674
  const entry = rawEntry;
56469
- return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
56675
+ return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType, events }) => isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
56470
56676
  }
56471
56677
  /**
56472
56678
  * Emit the payload fields specific to a hook type — `url`/`headers`/
@@ -56792,19 +56998,23 @@ function stripCommandPrefix({ command, args, shell, converterConfig, warn }) {
56792
56998
  const cmd = typeof command === "string" ? command : void 0;
56793
56999
  const { projectDirVar } = converterConfig;
56794
57000
  if (projectDirVar === "" || typeof cmd !== "string") return cmd;
56795
- const { command: stripped, unrestored, retained } = importProjectDirVariable({
56796
- command: cmd,
56797
- projectDirVar,
56798
- firstWordOnly: isExecFormHook({
56799
- args,
56800
- converterConfig
56801
- }) || !runsInPosixShell({
56802
- shell,
56803
- converterConfig
56804
- })
56805
- });
56806
- 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.`);
56807
- 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.`);
57001
+ let stripped = cmd;
57002
+ for (const variable of [projectDirVar, ...converterConfig.importedProjectDirVarAliases ?? []]) {
57003
+ const result = importProjectDirVariable({
57004
+ command: stripped,
57005
+ projectDirVar: variable,
57006
+ firstWordOnly: isExecFormHook({
57007
+ args,
57008
+ converterConfig
57009
+ }) || !runsInPosixShell({
57010
+ shell,
57011
+ converterConfig
57012
+ })
57013
+ });
57014
+ stripped = result.command;
57015
+ 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.`);
57016
+ 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.`);
57017
+ }
56808
57018
  return stripped;
56809
57019
  }
56810
57020
  /**
@@ -56845,7 +57055,7 @@ function isStringRecord(value) {
56845
57055
  }
56846
57056
  /** Compare object values without letting key order decide the answer. */
56847
57057
  function stableJson(value) {
56848
- if (typeof value === "string") return JSON.stringify(value);
57058
+ if (typeof value !== "object") return JSON.stringify(value);
56849
57059
  return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
56850
57060
  }
56851
57061
  /**
@@ -57085,6 +57295,7 @@ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn })
57085
57295
  }),
57086
57296
  ...importGroupPassthroughFields({
57087
57297
  rawEntry,
57298
+ eventName,
57088
57299
  converterConfig
57089
57300
  }),
57090
57301
  ...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
@@ -57137,15 +57348,26 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
57137
57348
  * is the group-level twin of `matcher`: importing the group without it would
57138
57349
  * widen every hook in it, so the group is skipped instead.
57139
57350
  */
57140
- function describeGroupSkipReason({ rawEntry, converterConfig }) {
57351
+ function describeGroupSkipReason({ rawEntry, eventName, converterConfig }) {
57141
57352
  const entry = rawEntry;
57142
- for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
57353
+ for (const { tool, valueType, subdividesGroup, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
57143
57354
  const value = entry[tool];
57144
- if (subdividesGroup !== true || value === void 0) continue;
57355
+ if (subdividesGroup !== true || onInvalidImport === "drop-field" || value === void 0 || !isGroupFieldOnEvent(events, eventName)) continue;
57145
57356
  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.`;
57146
57357
  }
57147
57358
  }
57148
57359
  /**
57360
+ * Warn about each `"drop-field"` group value that is present but unusable;
57361
+ * {@link importGroupPassthroughFields} leaves it off the imported hooks.
57362
+ */
57363
+ function warnDroppedGroupFields({ rawEntry, eventName, converterConfig, warn }) {
57364
+ const entry = rawEntry;
57365
+ for (const { tool, valueType, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
57366
+ const value = entry[tool];
57367
+ 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.`);
57368
+ }
57369
+ }
57370
+ /**
57149
57371
  * Why this hook cannot be imported, or `undefined` when it can.
57150
57372
  */
57151
57373
  function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
@@ -57172,12 +57394,19 @@ function toolMatcherEntryToCanonical({ rawEntry, eventName, converterConfig, war
57172
57394
  const hookDefs = rawEntry.hooks ?? [];
57173
57395
  const groupSkipReason = describeGroupSkipReason({
57174
57396
  rawEntry,
57397
+ eventName,
57175
57398
  converterConfig
57176
57399
  });
57177
57400
  if (groupSkipReason !== void 0) {
57178
57401
  warn?.(groupSkipReason);
57179
57402
  return [];
57180
57403
  }
57404
+ warnDroppedGroupFields({
57405
+ rawEntry,
57406
+ eventName,
57407
+ converterConfig,
57408
+ warn
57409
+ });
57181
57410
  const definitions = [];
57182
57411
  for (const h of hookDefs) {
57183
57412
  const skipReason = describeHookSkipReason({
@@ -60516,6 +60745,23 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
60516
60745
  });
60517
60746
  }
60518
60747
  };
60748
+ // src/features/hooks/hook-matcher-tool-names.ts
60749
+ const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60750
+ function isMatcherName(token) {
60751
+ return !token.startsWith("\\") && !token.startsWith("[");
60752
+ }
60753
+ /**
60754
+ * Rewrites the whole tool names inside a hook `matcher` regex through `names`,
60755
+ * leaving escapes, character classes and unknown names as written. Used by
60756
+ * adapters whose runtime tool names differ from the canonical (Claude Code)
60757
+ * spelling.
60758
+ */
60759
+ function replaceMatcherNames(matcher, names) {
60760
+ return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60761
+ }
60762
+ function invertToolNames(names) {
60763
+ return Object.fromEntries(Object.entries(names).map(([canonical, tool]) => [tool, canonical]));
60764
+ }
60519
60765
  // src/features/hooks/cortexcode-hooks.ts
60520
60766
  const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
60521
60767
  const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
@@ -60526,14 +60772,7 @@ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
60526
60772
  Grep: "grep",
60527
60773
  Glob: "glob"
60528
60774
  };
60529
- const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_TOOL_NAMES).map(([canonical, tool]) => [tool, canonical]));
60530
- const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60531
- function isMatcherName(token) {
60532
- return !token.startsWith("\\") && !token.startsWith("[");
60533
- }
60534
- function replaceMatcherNames(matcher, names) {
60535
- return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60536
- }
60775
+ const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60537
60776
  function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
60538
60777
  const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60539
60778
  const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
@@ -65694,6 +65933,61 @@ var TabnineHooks = class TabnineHooks extends ToolHooks {
65694
65933
  });
65695
65934
  }
65696
65935
  };
65936
+ // src/features/hooks/trae-hooks.ts
65937
+ const TRAE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
65938
+ "sessionStart",
65939
+ "beforeSubmitPrompt",
65940
+ "stop"
65941
+ ]);
65942
+ const CANONICAL_TO_TRAE_TOOL_NAMES = { Bash: "RunCommand" };
65943
+ const TRAE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_TRAE_TOOL_NAMES);
65944
+ const TRAE_SPEC = {
65945
+ displayName: "TRAE",
65946
+ overrideKey: "trae",
65947
+ converterConfig: {
65948
+ supportedEvents: TRAE_HOOK_EVENTS,
65949
+ canonicalToToolEventNames: CANONICAL_TO_TRAE_EVENT_NAMES,
65950
+ toolToCanonicalEventNames: TRAE_TO_CANONICAL_EVENT_NAMES,
65951
+ projectDirVar: "$TRAE_PROJECT_DIR",
65952
+ stripsProjectDirVarOnImportOnly: true,
65953
+ importedProjectDirVarAliases: ["$CLAUDE_PROJECT_DIR"],
65954
+ noMatcherEvents: TRAE_NO_MATCHER_EVENTS,
65955
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
65956
+ groupPassthroughFields: [{
65957
+ canonical: "loop_limit",
65958
+ tool: "loop_limit",
65959
+ valueType: "number",
65960
+ events: /* @__PURE__ */ new Set(["stop"]),
65961
+ subdividesGroup: true,
65962
+ onInvalidImport: "drop-field"
65963
+ }],
65964
+ matcherTranslation: {
65965
+ toTool: ({ matcher }) => replaceMatcherNames(matcher, CANONICAL_TO_TRAE_TOOL_NAMES),
65966
+ toCanonical: (matcher) => replaceMatcherNames(matcher, TRAE_TO_CANONICAL_TOOL_NAMES)
65967
+ }
65968
+ }
65969
+ };
65970
+ /**
65971
+ * TRAE hooks.
65972
+ *
65973
+ * Hooks live under the top-level `hooks` key of the dedicated `.trae/hooks.json`
65974
+ * (project) and `~/.trae/hooks.json` (user scope). The IDE also writes a
65975
+ * `version` key there, so generation merges the `hooks` key into the file (see
65976
+ * `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
65977
+ *
65978
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
65979
+ */
65980
+ var TraeHooks = class extends SettingsJsonHooks {
65981
+ static getSpec() {
65982
+ return TRAE_SPEC;
65983
+ }
65984
+ static getSettablePaths(_options = {}) {
65985
+ return {
65986
+ relativeDirPath: TRAE_DIR,
65987
+ relativeFilePath: TRAE_HOOKS_FILE_NAME
65988
+ };
65989
+ }
65990
+ };
65697
65991
  // src/features/hooks/vibe-hooks.ts
65698
65992
  const VIBE_DIR = ".vibe";
65699
65993
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -66878,6 +67172,21 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
66878
67172
  supportedHookTypes: ["command"],
66879
67173
  supportsMatcher: true
66880
67174
  }],
67175
+ ["trae", {
67176
+ class: TraeHooks,
67177
+ meta: {
67178
+ supportsProject: true,
67179
+ supportsGlobal: true,
67180
+ supportsImport: true
67181
+ },
67182
+ ...converterCapabilities(TraeHooks.getConverterConfig()),
67183
+ supportsMatcher: true,
67184
+ matcherEvents: [
67185
+ "preToolUse",
67186
+ "postToolUse",
67187
+ "notification"
67188
+ ]
67189
+ }],
66881
67190
  ["vibe", {
66882
67191
  class: VibeHooks,
66883
67192
  meta: {
@@ -68986,69 +69295,6 @@ var AiassistantMcp = class extends ToolMcp {
68986
69295
  });
68987
69296
  }
68988
69297
  };
68989
- // src/features/shared/amp-settings.ts
68990
- /**
68991
- * Parse an Amp `settings.json` / `settings.jsonc` document.
68992
- *
68993
- * Shared by the MCP and permissions adapters because both read the very same
68994
- * file. Two separately maintained copies drifted once already: the permissions
68995
- * side was hardened while the MCP side kept calling `jsonc-parser` directly,
68996
- * which left it importing MCP servers reachable only through an injected
68997
- * `__proto__`.
68998
- *
68999
- * {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
69000
- * error instead of returning `jsonc-parser`'s best-effort value, and it
69001
- * rebuilds every object from its own enumerable entries, so a root `__proto__`
69002
- * cannot swap the returned object's prototype and `constructor` / `prototype`
69003
- * never survive as own keys, at any depth.
69004
- *
69005
- * Removal is silent, matching every other tool-side adapter (opencode, kilo,
69006
- * copilot). Reporting the removed keys through `droppedPollutionKeysError` is
69007
- * reserved for the three files a user authors under `.rulesync/`, whose whole
69008
- * purpose is to be turned into tool config — a key that vanishes there needs
69009
- * explaining, whereas here the surrounding settings are the user's own file
69010
- * and are left alone.
69011
- *
69012
- * Note the consequence for a root `__proto__`: sanitizing runs before the
69013
- * plain-object check, so such a document parses successfully with the key
69014
- * stripped rather than failing that check. That is deliberate — dropping the
69015
- * one poisoned key keeps the user's unrelated settings — and matches the
69016
- * sanitize-before-the-root-check design the shared config gateway documents.
69017
- */
69018
- function parseAmpSettings({ fileContent }) {
69019
- let parsed;
69020
- try {
69021
- parsed = parseJsonc(fileContent || "{}");
69022
- } catch (error) {
69023
- throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
69024
- }
69025
- if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
69026
- return parsed;
69027
- }
69028
- /**
69029
- * Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
69030
- * an existing user file is read-modified-written in place instead of a fresh
69031
- * `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
69032
- * `settings.json` when neither file exists: Amp's canonically documented
69033
- * settings file (`.amp/settings.json`), shared by the MCP and permissions
69034
- * adapters, so generating both on a clean repo writes a single settings file.
69035
- */
69036
- async function resolveAmpSettingsFile(jsonDir) {
69037
- const jsoncContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
69038
- if (jsoncContent !== null) return {
69039
- fileContent: jsoncContent,
69040
- relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
69041
- };
69042
- const jsonContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_FILE_NAME));
69043
- if (jsonContent !== null) return {
69044
- fileContent: jsonContent,
69045
- relativeFilePath: AMP_SETTINGS_FILE_NAME
69046
- };
69047
- return {
69048
- fileContent: null,
69049
- relativeFilePath: AMP_SETTINGS_FILE_NAME
69050
- };
69051
- }
69052
69298
  // src/features/mcp/amp-mcp.ts
69053
69299
  const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
69054
69300
  function filterMcpServers(mcpServers) {
@@ -72034,7 +72280,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
72034
72280
  fileKey: sharedConfigFileKey(paths),
72035
72281
  feature: "mcp",
72036
72282
  existingContent,
72037
- patch: { servers: convertToCopilotFormat(mcpServers) },
72283
+ patch: { servers: undefinedWhenEmpty(convertToCopilotFormat(mcpServers)) },
72038
72284
  filePath,
72039
72285
  logger
72040
72286
  }),
@@ -74355,14 +74601,20 @@ function resolveGooseUrl(config) {
74355
74601
  }
74356
74602
  /**
74357
74603
  * Determines the Goose `type` for a server based on its command/url/transport.
74604
+ *
74605
+ * Goose no longer has an SSE extension type (it skips a `type: sse` entry as
74606
+ * unsupported), so every remote server is written as `streamable_http`.
74358
74607
  */
74359
74608
  function resolveGooseType(config, url) {
74360
74609
  if (config.command !== void 0) return "stdio";
74361
- if (url !== void 0) return canonicalTransport(config) === "sse" ? "sse" : "streamable_http";
74610
+ if (url !== void 0) return "streamable_http";
74362
74611
  return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
74363
74612
  }
74364
74613
  /**
74365
- * The Goose extension types that carry an MCP server. Goose also documents
74614
+ * The Goose extension types that carry an MCP server. `sse` is no longer one
74615
+ * Goose reads, but it stays here so a stale `type: sse` entry is still
74616
+ * recognized: imported as canonical `sse` and replaced by the regenerated
74617
+ * same-name `streamable_http` entry. Goose also documents
74366
74618
  * `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
74367
74619
  * no canonical MCP counterpart: they name capabilities Goose provides itself
74368
74620
  * rather than a server rulesync could describe.
@@ -74422,7 +74674,8 @@ function convertServerToGooseExtension(name, config, logger) {
74422
74674
  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.`);
74423
74675
  return;
74424
74676
  }
74425
- } else if (gooseType === "sse" || gooseType === "streamable_http") {
74677
+ } else if (gooseType === "streamable_http") {
74678
+ 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.`);
74426
74679
  if (url !== void 0) ext.uri = url;
74427
74680
  if (isPlainObject(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
74428
74681
  }
@@ -74435,8 +74688,9 @@ function convertServerToGooseExtension(name, config, logger) {
74435
74688
  * Converts rulesync canonical MCP servers into Goose `extensions:` entries.
74436
74689
  *
74437
74690
  * Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
74438
- * | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
74439
- * remote, plus `enabled` and `timeout`.
74691
+ * | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for remote, plus
74692
+ * `enabled` and `timeout`. A canonical `sse` server is written as
74693
+ * `streamable_http` with a warning, since Goose dropped SSE support.
74440
74694
  *
74441
74695
  * `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
74442
74696
  * holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
@@ -74567,7 +74821,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
74567
74821
  * `~/.config/goose/config.yaml`. That file also holds other Goose settings
74568
74822
  * (model, provider, ...), so generation merges the `extensions:` block into the
74569
74823
  * existing config instead of overwriting it, and the file is never deleted.
74570
- * This location supports both stdio and remote (http/sse) servers.
74824
+ * This location supports both stdio and remote servers (SSE is written as
74825
+ * `streamable_http`, since Goose no longer reads `type: sse`).
74571
74826
  * - **Project**: a stdio-only open-plugin manifest at
74572
74827
  * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
74573
74828
  * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
@@ -79285,6 +79540,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
79285
79540
  const managedNames = new Set(Object.keys(rulesyncServers));
79286
79541
  const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
79287
79542
  const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
79543
+ const servers = [...mcpServers, ...unmanagedServers];
79288
79544
  return new VibeMcp({
79289
79545
  outputRoot,
79290
79546
  relativeDirPath: paths.relativeDirPath,
@@ -79293,7 +79549,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
79293
79549
  fileKey: sharedConfigFileKey(paths),
79294
79550
  feature: "mcp",
79295
79551
  existingContent,
79296
- patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
79552
+ patch: { mcp_servers: servers.length > 0 ? servers : void 0 },
79297
79553
  filePath
79298
79554
  }),
79299
79555
  validate,
@@ -82717,6 +82973,20 @@ function isUnicodeClass(rest) {
82717
82973
  }));
82718
82974
  }
82719
82975
  /**
82976
+ * Return a function that, for a `[:` at `index`, gives the position of the
82977
+ * next `:]` (Go reads everything between as the class name), or -1. The
82978
+ * position is cached and `index` only grows, so a long run of `[:` with no
82979
+ * `:]` costs linear time rather than a scan to the end at each one.
82980
+ */
82981
+ function posixEndFinder(source) {
82982
+ let next = -2;
82983
+ return (index) => {
82984
+ if (!source.startsWith("[:", index)) return -1;
82985
+ if (next !== -1 && next < index + 2) next = source.indexOf(":]", index + 2);
82986
+ return next;
82987
+ };
82988
+ }
82989
+ /**
82720
82990
  * Replace each POSIX class (`[:alpha:]`) inside a bracket class with a plain
82721
82991
  * character, so JavaScript can compile the rest, or return `undefined` for an
82722
82992
  * unknown one. Only inside a bracket is `[:name:]` a POSIX class; a bare
@@ -82726,6 +82996,7 @@ function replacePosixClasses(source) {
82726
82996
  let result = "";
82727
82997
  let inClass = false;
82728
82998
  let index = 0;
82999
+ const findPosixEnd = posixEndFinder(source);
82729
83000
  while (index < source.length) {
82730
83001
  const character = source[index] ?? "";
82731
83002
  if (character === "\\") {
@@ -82750,11 +83021,11 @@ function replacePosixClasses(source) {
82750
83021
  }
82751
83022
  continue;
82752
83023
  }
82753
- const posix = /^\[:\^?([\s\S]*?):\]/.exec(source.slice(index));
82754
- if (posix !== null) {
82755
- if (!POSIX_CLASSES.has(posix[1] ?? "")) return;
83024
+ const posixEnd = findPosixEnd(index);
83025
+ if (posixEnd !== -1) {
83026
+ if (!POSIX_CLASSES.has(source.slice(index + 2, posixEnd).replace(/^\^/, ""))) return;
82756
83027
  result += "a";
82757
- index += posix[0].length;
83028
+ index = posixEnd + 2;
82758
83029
  continue;
82759
83030
  }
82760
83031
  if (character === "]") inClass = false;
@@ -82833,6 +83104,7 @@ function hasClassEscapeRangeEnd(source) {
82833
83104
  let inClass = false;
82834
83105
  let afterCharacter = false;
82835
83106
  let index = 0;
83107
+ const findPosixEnd = posixEndFinder(source);
82836
83108
  while (index < source.length) {
82837
83109
  const character = source[index] ?? "";
82838
83110
  if (character === "\\") {
@@ -82848,7 +83120,7 @@ function hasClassEscapeRangeEnd(source) {
82848
83120
  });
82849
83121
  continue;
82850
83122
  }
82851
- const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
83123
+ const posixEnd = findPosixEnd(index);
82852
83124
  if (character === "]" || posixEnd !== -1) {
82853
83125
  inClass = character !== "]";
82854
83126
  afterCharacter = false;
@@ -83410,6 +83682,8 @@ function buildToolPatternEntry({ toolName, pattern }) {
83410
83682
  * - `execute_url` takes a domain, like `read_url`.
83411
83683
  * - `command` and `unsandboxed` take literal words or a per-word `regex:`,
83412
83684
  * matched word by word (see `antigravity-command-patterns.ts`).
83685
+ * - `mcp` takes `server/tool` or `server/*`, matched exactly. A canonical
83686
+ * `mcp__<server>__<tool>` category is written as that target.
83413
83687
  *
83414
83688
  * A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
83415
83689
  * matches nothing, so a deny written that way blocks nothing. A pattern with
@@ -83433,7 +83707,8 @@ const DENY_HINTS = {
83433
83707
  read_file: "deny a directory or file path instead",
83434
83708
  write_file: "deny a directory or file path instead",
83435
83709
  command: "deny the first words of the command instead",
83436
- unsandboxed: "deny the first words of the command instead"
83710
+ unsandboxed: "deny the first words of the command instead",
83711
+ mcp: "deny the whole MCP server or tool instead"
83437
83712
  };
83438
83713
  function hasGlob(pattern) {
83439
83714
  return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
@@ -83489,6 +83764,40 @@ function toTarget(action, pattern) {
83489
83764
  if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
83490
83765
  return { target: pattern };
83491
83766
  }
83767
+ const MCP_CATEGORY_PREFIX = "mcp__";
83768
+ const MCP_TARGET_SEPARATORS = /[/()]/;
83769
+ /**
83770
+ * The `mcp` target of a canonical `mcp__<server>__<tool>` category:
83771
+ * `<server>/<tool>`, `<server>/*` for a whole server (`mcp__<server>`,
83772
+ * `mcp__<server>__*`), or `*` for every MCP tool (`mcp__*`, `mcp__*__*`). The
83773
+ * server ends at the first `__`, as elsewhere in rulesync. Antigravity matches
83774
+ * names exactly, so a glob, an empty name, or a `/` or parenthesis in a name
83775
+ * has no target; when only the tool part is at fault, its server is the `lead`
83776
+ * a deny widens to.
83777
+ */
83778
+ function toMcpTarget(category) {
83779
+ const remainder = category.slice(5);
83780
+ const separator = remainder.indexOf("__");
83781
+ const server = separator === -1 ? remainder : remainder.slice(0, separator);
83782
+ const tool = separator === -1 ? "*" : remainder.slice(separator + 2);
83783
+ if (server === "*" && tool === "*") return { target: "*" };
83784
+ 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";
83785
+ if (server.length === 0 || MCP_TARGET_SEPARATORS.test(server) || hasGlob(server)) return { skipReason };
83786
+ if (tool === "*") return { target: `${server}/*` };
83787
+ return tool.length === 0 || MCP_TARGET_SEPARATORS.test(tool) || hasGlob(tool) ? {
83788
+ skipReason,
83789
+ lead: `${server}/*`
83790
+ } : { target: `${server}/${tool}` };
83791
+ }
83792
+ /** The target of a canonical `mcp__...` rule, with the `lead` a deny widens to. */
83793
+ function toMcpRuleTarget(category, pattern) {
83794
+ const target = toMcpTarget(category);
83795
+ if (pattern === "*" || !("target" in target)) return target;
83796
+ return {
83797
+ skipReason: "Antigravity MCP targets name a server or a tool, not the tool's arguments",
83798
+ lead: target.target
83799
+ };
83800
+ }
83492
83801
  /**
83493
83802
  * The leading directories of a path glob (`src` for `src/*.ts`), or
83494
83803
  * `undefined` when it has none. A directory target covers everything inside
@@ -83511,6 +83820,20 @@ function toWidenedDenyTarget(action, pattern) {
83511
83820
  if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
83512
83821
  if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
83513
83822
  }
83823
+ /** The entry a deny is widened to, and what it now blocks. */
83824
+ function describeWidenedDeny({ action, prefix, isMcp }) {
83825
+ if (isMcp) return prefix === "*" ? {
83826
+ entry: action,
83827
+ scope: "every MCP tool"
83828
+ } : {
83829
+ entry: `${action}(${prefix})`,
83830
+ scope: `every call of \`${action}(${prefix})\``
83831
+ };
83832
+ return {
83833
+ entry: `${action}(${prefix})`,
83834
+ scope: PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``
83835
+ };
83836
+ }
83514
83837
  /**
83515
83838
  * Warn about a path target that is a home or variable reference. Antigravity
83516
83839
  * documents absolute and workspace-relative paths only, so such a reference
@@ -83532,14 +83855,20 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
83532
83855
  * nothing enforces it, and a widened one is warned about too, because it
83533
83856
  * blocks more than it says.
83534
83857
  */
83535
- function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
83536
- const result = pattern === "*" ? { target: "*" } : toTarget(action, pattern);
83858
+ function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
83859
+ const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
83860
+ const action = isMcp ? "mcp" : toolAction;
83861
+ const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
83537
83862
  const rule = `${category}: { "${pattern}": "${decision}" }`;
83538
83863
  if ("skipReason" in result) {
83539
- const prefix = decision === "deny" ? toWidenedDenyTarget(action, pattern) : void 0;
83864
+ const prefix = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
83540
83865
  if (prefix !== void 0) {
83541
- const scope = PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``;
83542
- logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
83866
+ const { entry, scope } = describeWidenedDeny({
83867
+ action,
83868
+ prefix,
83869
+ isMcp
83870
+ });
83871
+ logger.warn(`${toolLabel} permissions: widening ${rule} to ${entry}. ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
83543
83872
  warnHomeOrVariablePath({
83544
83873
  action,
83545
83874
  target: prefix,
@@ -83548,7 +83877,7 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
83548
83877
  logger,
83549
83878
  toolLabel
83550
83879
  });
83551
- return `${action}(${prefix})`;
83880
+ return entry;
83552
83881
  }
83553
83882
  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`;
83554
83883
  logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
@@ -83692,6 +84021,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
83692
84021
  mcp: "mcp"
83693
84022
  };
83694
84023
  function toAntigravityCliToolName(canonical) {
84024
+ if (canonical.startsWith("mcp__")) return "mcp";
83695
84025
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
83696
84026
  }
83697
84027
  function toCanonicalToolName$9(cliName) {
@@ -83754,10 +84084,11 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
83754
84084
  logger
83755
84085
  });
83756
84086
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
84087
+ const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
83757
84088
  const existingPermissions = settings.permissions ?? {};
83758
- const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83759
- const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83760
- const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
84089
+ const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
84090
+ const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
84091
+ const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
83761
84092
  const mergedPermissions = { ...existingPermissions };
83762
84093
  const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
83763
84094
  const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
@@ -83866,6 +84197,7 @@ const IDE_ACTION_TO_CANONICAL = {
83866
84197
  mcp: "mcp"
83867
84198
  };
83868
84199
  function toIdeAction(canonical) {
84200
+ if (canonical.startsWith("mcp__")) return "mcp";
83869
84201
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
83870
84202
  }
83871
84203
  function toCanonicalCategory$2(ideAction) {
@@ -83922,8 +84254,9 @@ var AntigravityIdePermissions = class AntigravityIdePermissions extends ToolPerm
83922
84254
  logger
83923
84255
  });
83924
84256
  const managedActions = new Set(Object.keys(config.permission).map((category) => toIdeAction(category)));
84257
+ const isManagedAction = (action) => managedActions.has(action) || managedActions.has("mcp") && action.startsWith("mcp__");
83925
84258
  const existingPermissions = settings.permissions ?? {};
83926
- const preserve = (entries) => (entries ?? []).filter((entry) => !managedActions.has(parseToolPatternEntry(entry).toolName));
84259
+ const preserve = (entries) => (entries ?? []).filter((entry) => !isManagedAction(parseToolPatternEntry(entry).toolName));
83927
84260
  const mergedPermissions = { ...existingPermissions };
83928
84261
  const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
83929
84262
  const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
@@ -107934,4 +108267,4 @@ async function importChecksCore(params) {
107934
108267
  }
107935
108268
  export { RulesyncPermissions as $, formatError as $n, isSymlink as $t, CODEBUDDY_DIR as A, MAX_FILE_SIZE as An, withFallbackLoggerTarget as At, CLAUDECODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Bn, assertTreeContainsNoSymlinks as Bt, QWENCODE_LOCAL_RULE_FILE_NAME as C, PACKAGING_TOOL_TARGETS as Cn, getSourceTransport as Ct, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as D, DEPRECATED_FEATURE_REPLACEMENTS as Dn, WarningCollectingLogger as Dt, FACTORYDROID_DIR as E, ALL_FEATURES_WITH_WILDCARD as En, JsonLogger as Et, groupSpellingsByCaseFoldedIdentity as F, RULESYNC_CONFIG_SCHEMA_URL as Fn, PathOutsideRootError as Ft, toInstructionBudgetFile as G, RULESYNC_PERMISSIONS_SCHEMA_URL as Gn, ensureDir as Gt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as H, RULESYNC_MCP_SCHEMA_URL as Hn, checkPathTraversal as Ht, CODEXCLI_BASH_RULES_FILE_NAME as I, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as In, applyFileMode as It, RulesyncSubagentFrontmatterSchema as J, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Jn, findFilesByGlobs as Jt, getLocalSkillDirNames as K, RULESYNC_RELATIVE_DIR_PATH as Kn, fileExists as Kt, CODEXCLI_DIR as L, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Ln, assertDirectoryIfExists as Lt, AUGMENTCODE_DIR as M, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Mn, withWarnOnceScope as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Nn, CLIError as Nt, COMMANDCODE_DIR as O, CURATED_RULES_FEATURE_SUBDIR as On, fallbackLogger as Ot, caseFoldIdentity as P, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Pn, ErrorCodes as Pt, RulesyncRuleFrontmatterSchema as Q, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Qn, isFileSystemError as Qt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as R, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Rn, assertNoSymlinkBelowRoot as Rt, QWENCODE_DIR as S, ALL_TOOL_TARGETS_WITH_WILDCARD as Sn, assertTargetsFeaturesExclusive as St, LETTACODE_SETTINGS_LOCAL_FILE_NAME as T, ALL_FEATURES as Tn, ConsoleLogger as Tt, CLAUDECODE_SKILLS_DIR_PATH as U, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Un, createTempDirectory as Ut, CLAUDECODE_MEMORIES_DIR_NAME as V, RULESYNC_MCP_RELATIVE_FILE_PATH as Vn, assertWritablePathInsideRoot as Vt, findProjectInstructionBudgetFindings as W, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Wn, directoryExists as Wt, RulesyncSkillFrontmatterSchema as X, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Xn, getHomeDirectory as Xt, RulesyncSkill as Y, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Yn, getFileSizeAndExecutableBit as Yt, RulesyncRule as Z, RULESYNC_USER_CONFIG_DIR_NAME as Zn, isFileNotFoundError as Zt, SubagentsProcessor as _, toPosixPath as _n, resolveEffectiveInputRoots as _t, convertFromTool as a, readFileContent as an, stripControlCharactersKeepingLineFeeds as ar, parseJsonc as at, CommandsProcessor as b, mapWithConcurrency as bn, GITIGNORE_DESTINATION_KEY as bt, parseCommaSeparatedList as c, removeDirectoryStrict as cn, RulesyncCheck as ct, shortenToWidth as d, removeFileWithoutFollowingSymlinks as dn, loadYaml as dt, listDirectoryEntryNames as en, truncateText as er, RulesyncMcp as et, McpProcessor as f, removeTempDirectory as fn, isRecord as ft, RulesProcessor as g, splitPathSegments as gn, mergeInputRootConfigs as gt, ChecksProcessor as h, runWithDirectoryRollback as hn, ConfigResolver as ht, inspectInputRoots as i, posixRelativePathEscapesRoot as in, stripControlCharacters as ir, resolveRulesyncSourceWritePath as it, CODEBUDDY_LOCAL_RULE_FILE_NAME as j, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as jn, resetRunWarningState as jt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as k, FEATURE_SOURCE_TREE_ENTRIES as kn, warnOnConflictingFlags as kt, ELLIPSIS_WIDTH as l, removeFile as ln, RulesyncCheckFrontmatterSchema as lt, HooksProcessor as m, resolvedRelativePath as mn, SKILL_FILE_NAME as mt, formatSourceLoadFailure as n, listSubdirectoryNames as nn, hasEnclosingMarkOutsideKeycap as nr, RulesyncHooks as nt, isPackagingToolTarget as o, readFileContentOrNull as on, stripControlCharactersKeepingLineFeedsAndTabs as or, RulesyncCommand as ot, IgnoreProcessor as p, resolvePath as pn, SHARED_USER_MANAGED_CONFIG_PATHS as pt, RulesyncSubagent as q, RULESYNC_RULES_RELATIVE_DIR_PATH as qn, fileExistsStrict as qt, generate as r, pathEscapesRoot as rn, quoteForLog as rr, getRulesyncSourceCandidates as rt, getProcessorRegistryEntry as s, removeDirectory as sn, stripHiddenCharacters as sr, RulesyncCommandFrontmatterSchema as st, importFromTool as t, listFilePathsRecursively as tn, hasDeceptiveHiddenCharacters as tr, RulesyncIgnore as tt, displayWidthOf as u, removeFileStrict as un, stringifyFrontmatter as ut, SkillsProcessor as v, writeFileBuffer as vn, CONFLICTING_TARGET_PAIRS as vt, LETTACODE_DIR as w, ToolTargetSchema as wn, findControlCharacter as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, ALL_TOOL_TARGETS as xn, SourceEntrySchema as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, writeFileContent as yn, ConfigFileSchema as yt, CLAUDECODE_DIR as z, RULESYNC_IGNORE_RELATIVE_FILE_PATH as zn, assertPathResolvesInsideRoot as zt };
107936
108269
 
107937
- //# sourceMappingURL=import-C_XK2L62.js.map
108270
+ //# sourceMappingURL=import--Y_A56qx.js.map