rulesync 43.3.0 → 43.4.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.
@@ -5635,11 +5635,28 @@ const POOL_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_POOL_EVENT_N
5635
5635
  /**
5636
5636
  * Hook events supported by Crush.
5637
5637
  *
5638
- * Crush's `hooks` config block currently fires a single event, `PreToolUse`
5639
- * (Claude Code-compatible payload; `matcher` is a regex on the tool name).
5640
- * @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
5638
+ * Crush v0.99.0 fires 13 events under its `hooks` config block (Claude
5639
+ * Code-compatible payload; `matcher` is a regex on the tool name and is only
5640
+ * accepted on tool events). Twelve of them have a canonical counterpart;
5641
+ * `PermissionResult` has none and is authored under the `crush.hooks`
5642
+ * override.
5643
+ * @see https://github.com/charmbracelet/crush/blob/v0.99.0/docs/hooks/README.md
5644
+ * @see https://github.com/charmbracelet/crush/blob/v0.99.0/internal/hooks/events.go
5641
5645
  */
5642
- const CRUSH_HOOK_EVENTS = ["preToolUse"];
5646
+ const CRUSH_HOOK_EVENTS = [
5647
+ "preToolUse",
5648
+ "postToolUse",
5649
+ "postToolUseFailure",
5650
+ "permissionRequest",
5651
+ "sessionStart",
5652
+ "sessionEnd",
5653
+ "beforeSubmitPrompt",
5654
+ "stop",
5655
+ "stopFailure",
5656
+ "stopCancelled",
5657
+ "preCompact",
5658
+ "postCompact"
5659
+ ];
5643
5660
  /**
5644
5661
  * Hook events supported by JetBrains Junie CLI.
5645
5662
  *
@@ -5904,9 +5921,10 @@ const ZCODE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_ZCODE_EVENT
5904
5921
  * Bob reads `hooks` from `.bob/settings.json` (project) and
5905
5922
  * `~/.bob/settings/settings.json` (user) in the Claude-Code shape and fires
5906
5923
  * seven events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
5907
- * PreCompact, PostCompact (both added by Bob Shell 2.0.3) and Stop. Handlers
5908
- * are `command` or `https` hooks, and `matcher` (a regex over the tool name)
5909
- * applies to the two tool events.
5924
+ * PreCompact, PostCompact and Stop (Bob IDE and Bob Shell list the same
5925
+ * seven). Handlers are `command` or `http` hooks. `matcher` is a regex matched
5926
+ * against the tool name on PreToolUse / PostToolUse, `source` on SessionStart
5927
+ * and `trigger` on PreCompact / PostCompact; the other events ignore it.
5910
5928
  *
5911
5929
  * @see https://bob.ibm.com/docs/shell/configuration/lifecycle-hooks
5912
5930
  * @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
@@ -7088,10 +7106,38 @@ const CANONICAL_TO_CODEWHALE_EVENT_NAMES = {
7088
7106
  const CODEWHALE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CODEWHALE_EVENT_NAMES);
7089
7107
  /**
7090
7108
  * Canonical -> Crush event names. Crush spells its events the Claude Code
7091
- * way (`PreToolUse`).
7092
- * @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
7109
+ * way (`PreToolUse`), but names the turn events `TurnStart` (Claude Code's
7110
+ * `UserPromptSubmit`) and `TurnEnd` (Claude Code's `Stop`), and the
7111
+ * user-cancelled turn `Interrupt`.
7112
+ * @see https://github.com/charmbracelet/crush/blob/v0.99.0/docs/hooks/README.md
7113
+ */
7114
+ const CANONICAL_TO_CRUSH_EVENT_NAMES = {
7115
+ preToolUse: "PreToolUse",
7116
+ postToolUse: "PostToolUse",
7117
+ postToolUseFailure: "PostToolUseFailure",
7118
+ permissionRequest: "PermissionRequest",
7119
+ sessionStart: "SessionStart",
7120
+ sessionEnd: "SessionEnd",
7121
+ beforeSubmitPrompt: "TurnStart",
7122
+ stop: "TurnEnd",
7123
+ stopFailure: "StopFailure",
7124
+ stopCancelled: "Interrupt",
7125
+ preCompact: "PreCompact",
7126
+ postCompact: "PostCompact"
7127
+ };
7128
+ /**
7129
+ * Crush events that carry a tool call, the only ones whose hooks may set a
7130
+ * `matcher` (Crush's `EventHasTool`). A matcher on any other event makes
7131
+ * Crush reject the whole config at load time.
7132
+ * @see https://github.com/charmbracelet/crush/blob/v0.99.0/internal/config/hooks.go
7093
7133
  */
7094
- const CANONICAL_TO_CRUSH_EVENT_NAMES = { preToolUse: "PreToolUse" };
7134
+ const CRUSH_TOOL_EVENTS = [
7135
+ "PreToolUse",
7136
+ "PostToolUse",
7137
+ "PostToolUseFailure",
7138
+ "PermissionRequest",
7139
+ "PermissionResult"
7140
+ ];
7095
7141
  const CRUSH_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CRUSH_EVENT_NAMES);
7096
7142
  /**
7097
7143
  * Map canonical camelCase event names to Qwen Code PascalCase.
@@ -7635,14 +7681,18 @@ const toolScopedMcpSchema = zod_mini.z.looseObject({ mcpServers: zod_mini.z.opti
7635
7681
  * server opts out. Per-server `startupTimeoutMs` / `toolTimeoutMs` / `deferred`
7636
7682
  * in `mcpServers` still win. `defaultDeferred` only takes effect when Kimi's
7637
7683
  * experimental `tool-select` flag is on, and is captured when a server connects
7638
- * (MoonshotAI/kimi-code#4210).
7684
+ * (MoonshotAI/kimi-code#4210). `reconnectInitialDelayMs` / `reconnectMaxDelayMs`
7685
+ * (Kimi Code 2.4.0) set the first delay and the cap of the automatic reconnect
7686
+ * backoff for workspace-level servers (MoonshotAI/kimi-code#4283).
7639
7687
  *
7640
7688
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
7641
7689
  */
7642
7690
  const kimiCodeScopedMcpSchema = zod_mini.z.extend(toolScopedMcpSchema, {
7643
7691
  startupTimeoutMs: zod_mini.z.optional(zod_mini.z.number()),
7644
7692
  toolTimeoutMs: zod_mini.z.optional(zod_mini.z.number()),
7645
- defaultDeferred: zod_mini.z.optional(zod_mini.z.boolean())
7693
+ defaultDeferred: zod_mini.z.optional(zod_mini.z.boolean()),
7694
+ reconnectInitialDelayMs: zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.nonnegative())),
7695
+ reconnectMaxDelayMs: zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.nonnegative()))
7646
7696
  });
7647
7697
  /**
7648
7698
  * Qwen Code's tool-scoped block also carries the server-level allow/deny lists
@@ -11468,6 +11518,7 @@ const piSkillSectionSchema = zod_mini.z.looseObject({
11468
11518
  });
11469
11519
  const antigravitySkillSectionSchema = zod_mini.z.looseObject({
11470
11520
  "disable-slash-command": zod_mini.z.optional(zod_mini.z.boolean()),
11521
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
11471
11522
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
11472
11523
  });
11473
11524
  const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
@@ -16227,8 +16278,9 @@ var ToolSkill = class extends AiDir {
16227
16278
  * {@link AntigravitySharedSkill.getToolTarget}.
16228
16279
  *
16229
16280
  * Frontmatter beyond `name`/`description` (e.g. `disable-slash-command`,
16230
- * `metadata`) comes from the rulesync section named after that target, with the
16231
- * root-level `user-invocable` / `metadata` as defaults. On the shared project
16281
+ * `disable-model-invocation`, `metadata`) comes from the rulesync section named
16282
+ * after that target, with the root-level `user-invocable` /
16283
+ * `disable-model-invocation` / `metadata` as defaults. On the shared project
16232
16284
  * `.agents/skills/` tree both targets merge the `antigravity-ide` and
16233
16285
  * `antigravity-cli` sections so their output never diverges.
16234
16286
  */
@@ -16301,6 +16353,8 @@ var AntigravitySharedSkill = class extends ToolSkill {
16301
16353
  const { name: _name, description: _description, ...sectionFields } = section;
16302
16354
  const sectionDisableSlashCommand = sectionFields["disable-slash-command"];
16303
16355
  const disableSlashCommand = typeof sectionDisableSlashCommand === "boolean" ? sectionDisableSlashCommand : rulesyncFrontmatter["user-invocable"] === false ? true : void 0;
16356
+ const sectionDisableModelInvocation = sectionFields["disable-model-invocation"];
16357
+ const disableModelInvocation = typeof sectionDisableModelInvocation === "boolean" ? sectionDisableModelInvocation : rulesyncFrontmatter["disable-model-invocation"];
16304
16358
  const metadata = resolveMetadata({
16305
16359
  rootFrontmatter: rulesyncFrontmatter,
16306
16360
  section: sectionFields
@@ -16310,6 +16364,7 @@ var AntigravitySharedSkill = class extends ToolSkill {
16310
16364
  name: rulesyncFrontmatter.name,
16311
16365
  description: rulesyncFrontmatter.description,
16312
16366
  ...disableSlashCommand !== void 0 && { "disable-slash-command": disableSlashCommand },
16367
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
16313
16368
  ...metadata !== void 0 && { metadata }
16314
16369
  };
16315
16370
  return new this({
@@ -31738,16 +31793,17 @@ const VibeSkillFrontmatterSchema = zod_mini.z.looseObject({
31738
31793
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
31739
31794
  });
31740
31795
  /**
31741
- * Skill names Vibe's built-in skills occupy. `SkillManager` treats them as
31742
- * reserved: a project or user skill whose frontmatter `name` matches one is
31743
- * skipped at load time with only a debug log ("Skipping skill '<name>' ...
31744
- * because builtin skill names are reserved"), so a generated skill by that
31745
- * name is silently never offered. That is the legacy backend, still the
31746
- * default; the unified harness constructs the manager with
31747
- * `include_builtins=False` because its built-ins arrive as skills of the
31748
- * shipped `vibe` plugin under namespaced names, so it reserves nothing.
31749
- * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/skills/builtins/__init__.py
31750
- * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/skills/manager.py
31796
+ * Skill names Vibe's built-in skills occupy. Before v3.0.4, `SkillManager`
31797
+ * treats them as reserved: a project or user skill whose frontmatter `name`
31798
+ * matches one is skipped at load time with only a debug log ("Skipping skill
31799
+ * '<name>' ... because builtin skill names are reserved"), so a generated
31800
+ * skill by that name is silently never offered. v3.0.4 removed
31801
+ * `_reserved_builtins` and that skip ("A user skill named vibe or
31802
+ * skill-creator is no longer silently skipped"), so the warning below is
31803
+ * scoped to older Vibe releases.
31804
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/vibe/core/skills/manager.py
31805
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.4/vibe/core/skills/manager.py
31806
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.4/CHANGELOG.md
31751
31807
  */
31752
31808
  const VIBE_RESERVED_SKILL_NAMES = /* @__PURE__ */ new Set(["vibe", "skill-creator"]);
31753
31809
  /**
@@ -31790,11 +31846,12 @@ function buildVibeFrontmatter(rulesyncFrontmatter) {
31790
31846
  /**
31791
31847
  * The skill is still generated under the reserved name — the name is the
31792
31848
  * user's to change, and a rename here would make it diverge from the other
31793
- * targets — but Vibe would drop it without a visible word, so say so now.
31849
+ * targets — but Vibe before v3.0.4 would drop it without a visible word, so
31850
+ * say so now.
31794
31851
  */
31795
31852
  function warnAboutReservedSkillName({ name, logger }) {
31796
31853
  if (!VIBE_RESERVED_SKILL_NAMES.has(name)) return;
31797
- logger?.warn(`Vibe skills: the skill name ${quoteValueForWarning(name)} is reserved for a Vibe built-in skill, so Vibe skips a project or user skill by that name when it loads skills. Rename the skill for it to be available in Vibe.`);
31854
+ logger?.warn(`Vibe skills: the skill name ${quoteValueForWarning(name)} matches a Vibe built-in skill. Vibe before v3.0.4 skips a project or user skill by that name when it loads skills; upgrade Vibe to v3.0.4 or later, or rename the skill, for it to be available.`);
31798
31855
  }
31799
31856
  var VibeSkill = class VibeSkill extends ToolSkill {
31800
31857
  constructor({ outputRoot = process.cwd(), relativeDirPath = VibeSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -42762,20 +42819,35 @@ const OPENCODE_KNOWN_SERVER_KEYS = /* @__PURE__ */ new Set([
42762
42819
  "disabledTools"
42763
42820
  ]);
42764
42821
  function convertFromOpencodeFormat(opencodeMcp, tools, remoteType) {
42822
+ const serverNames = Object.keys(opencodeMcp);
42765
42823
  return {
42766
- ...orphanMcpToolFiltersToRulesync(opencodeMcp, tools),
42824
+ ...orphanMcpToolFiltersToRulesync(opencodeMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findOpencodeToolOwner({
42825
+ key,
42826
+ serverNames
42827
+ })))),
42767
42828
  ...convertOpencodeServers(opencodeMcp, tools, remoteType)
42768
42829
  };
42769
42830
  }
42831
+ /** Recognize both OpenCode's sanitized names and keys older rulesync versions wrote. */
42832
+ function findOpencodeToolOwner({ key, serverNames }) {
42833
+ return findOpencodeMcpToolKeyOwner({
42834
+ key,
42835
+ serverNames,
42836
+ builtinKeys: /* @__PURE__ */ new Set(),
42837
+ acceptRawPrefix: true
42838
+ });
42839
+ }
42770
42840
  /** Split the shared top-level `tools` map into this server's own two lists. */
42771
- function splitOpencodeServerTools(serverName, tools) {
42841
+ function splitOpencodeServerTools(serverName, serverNames, tools) {
42772
42842
  const enabledTools = [];
42773
42843
  const disabledTools = [];
42774
- const prefix = `${serverName}_`;
42775
42844
  for (const [toolName, enabled] of Object.entries(tools ?? {})) {
42776
- if (!toolName.startsWith(prefix)) continue;
42777
- const toolSuffix = toolName.slice(prefix.length);
42778
- (enabled ? enabledTools : disabledTools).push(toolSuffix);
42845
+ const owner = findOpencodeToolOwner({
42846
+ key: toolName,
42847
+ serverNames
42848
+ });
42849
+ if (owner?.serverName !== serverName) continue;
42850
+ (enabled ? enabledTools : disabledTools).push(owner.toolName);
42779
42851
  }
42780
42852
  return {
42781
42853
  enabledTools,
@@ -42785,7 +42857,7 @@ function splitOpencodeServerTools(serverName, tools) {
42785
42857
  function convertOpencodeServers(opencodeMcp, tools, remoteType) {
42786
42858
  return Object.fromEntries(Object.entries(opencodeMcp).map(([serverName, serverConfig]) => {
42787
42859
  const extraFields = Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !OPENCODE_KNOWN_SERVER_KEYS.has(key)));
42788
- const { enabledTools, disabledTools } = splitOpencodeServerTools(serverName, tools);
42860
+ const { enabledTools, disabledTools } = splitOpencodeServerTools(serverName, Object.keys(opencodeMcp), tools);
42789
42861
  if (!isOpencodeTransportServer(serverConfig)) return [serverName, {
42790
42862
  ...extraFields,
42791
42863
  disabled: serverConfig.enabled === false,
@@ -42921,8 +42993,8 @@ function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
42921
42993
  return {
42922
42994
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
42923
42995
  const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], configShape, logger);
42924
- if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
42925
- if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
42996
+ if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[opencodeMcpToolPermissionKey(serverName, tool)] = true;
42997
+ if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[opencodeMcpToolPermissionKey(serverName, tool)] = false;
42926
42998
  return converted === null ? null : [serverName, converted];
42927
42999
  }).filter((entry) => entry !== null)),
42928
43000
  tools
@@ -55950,12 +56022,23 @@ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
55950
56022
  * into separate groups, so a restricting field is never inherited by a hook
55951
56023
  * that did not ask for it.
55952
56024
  */
55953
- function groupDefinitionsByMatcher({ definitions, converterConfig }) {
56025
+ function groupDefinitionsByMatcher({ definitions, eventName, converterConfig, warn }) {
55954
56026
  const subdividingFields = (converterConfig.groupPassthroughFields ?? []).filter(({ subdividesGroup }) => subdividesGroup);
55955
56027
  const byMatcher = /* @__PURE__ */ new Map();
56028
+ const translatedMatchers = /* @__PURE__ */ new Map();
55956
56029
  for (const def of definitions) {
55957
56030
  const rawMatcher = def.matcher ?? "";
55958
- const matcher = converterConfig.wildcardMatcherMeansAll && rawMatcher === "*" ? "" : rawMatcher;
56031
+ const normalizedMatcher = converterConfig.wildcardMatcherMeansAll && rawMatcher === "*" ? "" : rawMatcher;
56032
+ const translation = converterConfig.matcherTranslation;
56033
+ let matcher = normalizedMatcher;
56034
+ if (translation && normalizedMatcher && !converterConfig.noMatcherEvents?.has(eventName)) {
56035
+ matcher = translatedMatchers.get(normalizedMatcher) ?? translation.toTool({
56036
+ matcher: normalizedMatcher,
56037
+ eventName,
56038
+ warn
56039
+ });
56040
+ translatedMatchers.set(normalizedMatcher, matcher);
56041
+ }
55959
56042
  const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
55960
56043
  const value = def[canonical];
55961
56044
  return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
@@ -56465,7 +56548,7 @@ function buildToolHooks({ defs, eventName, converterConfig, warn }) {
56465
56548
  converterConfig,
56466
56549
  warn
56467
56550
  }),
56468
- type: converterConfig.hookTypeNames?.[hookType] ?? hookType,
56551
+ type: hookType,
56469
56552
  ...command !== void 0 && command !== null && { command },
56470
56553
  ...def.timeout !== void 0 && def.timeout !== null && { timeout: emitTimeout({
56471
56554
  timeout: def.timeout,
@@ -56515,7 +56598,9 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
56515
56598
  }) ?? eventName;
56516
56599
  const byMatcher = groupDefinitionsByMatcher({
56517
56600
  definitions,
56518
- converterConfig
56601
+ eventName,
56602
+ converterConfig,
56603
+ warn
56519
56604
  });
56520
56605
  const entries = [];
56521
56606
  const isNoMatcherEvent = converterConfig.noMatcherEvents?.has(eventName) ?? false;
@@ -56594,16 +56679,19 @@ function isImportedHookType(value) {
56594
56679
  return typeof value === "string" && IMPORTED_HOOK_TYPES.has(value);
56595
56680
  }
56596
56681
  /**
56597
- * The canonical hook type a tool-side `type` value denotes: the tool's own
56598
- * spelling from {@link ToolHooksConverterConfig.hookTypeNames} when it has
56599
- * one, otherwise the value itself when it is a canonical type, and `command`
56600
- * for anything else (the same coercion as before the renames existed).
56682
+ * The canonical hook type a tool-side `type` value denotes: the canonical type
56683
+ * an {@link ToolHooksConverterConfig.importedHookTypeAliases} entry maps it to
56684
+ * when it has one, otherwise the value itself when it is a canonical type, and
56685
+ * `command` for anything else.
56601
56686
  */
56602
56687
  function resolveImportedHookType({ value, converterConfig }) {
56603
- const names = converterConfig.hookTypeNames;
56604
- if (names !== void 0 && typeof value === "string") {
56605
- for (const [canonical, tool] of Object.entries(names)) if (tool === value) return canonical;
56606
- if (Object.hasOwn(names, value)) return "command";
56688
+ const aliases = converterConfig.importedHookTypeAliases;
56689
+ if (aliases !== void 0 && typeof value === "string") {
56690
+ const aliased = lookupOwn({
56691
+ record: aliases,
56692
+ key: value
56693
+ });
56694
+ if (aliased !== void 0) return aliased;
56607
56695
  }
56608
56696
  return isImportedHookType(value) ? value : "command";
56609
56697
  }
@@ -56831,7 +56919,7 @@ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn })
56831
56919
  canonical: "description",
56832
56920
  warn
56833
56921
  });
56834
- const matcher = rawEntry.matcher;
56922
+ const matcher = typeof rawEntry.matcher === "string" && converterConfig.matcherTranslation ? converterConfig.matcherTranslation.toCanonical(rawEntry.matcher) : rawEntry.matcher;
56835
56923
  return {
56836
56924
  type: hookType,
56837
56925
  ...command !== void 0 && command !== null && { command },
@@ -58329,15 +58417,9 @@ const BOB_SPEC = {
58329
58417
  canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
58330
58418
  toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
58331
58419
  projectDirVar: "",
58332
- noMatcherEvents: /* @__PURE__ */ new Set([
58333
- "sessionStart",
58334
- "beforeSubmitPrompt",
58335
- "preCompact",
58336
- "postCompact",
58337
- "stop"
58338
- ]),
58420
+ noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
58339
58421
  supportedHookTypes: /* @__PURE__ */ new Set(["command", "http"]),
58340
- hookTypeNames: { http: "https" },
58422
+ importedHookTypeAliases: { https: "http" },
58341
58423
  wildcardMatcherMeansAll: true
58342
58424
  }
58343
58425
  };
@@ -58349,7 +58431,7 @@ const BOB_SPEC = {
58349
58431
  * `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
58350
58432
  * `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
58351
58433
  * "command": "...", "timeout"?: <seconds> }] }] }`, where a handler may
58352
- * instead be `{ "type": "https", "url": "...", "timeout"?: <seconds> }`. The
58434
+ * instead be `{ "type": "http", "url": "https://...", "timeout"?: <seconds> }`. The
58353
58435
  * file also holds settings rulesync does not own, so generation merges the
58354
58436
  * `hooks` key into it (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting
58355
58437
  * the file.
@@ -60291,6 +60373,32 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
60291
60373
  }
60292
60374
  };
60293
60375
  // src/features/hooks/cortexcode-hooks.ts
60376
+ const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
60377
+ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
60378
+ Bash: "bash",
60379
+ Read: "read",
60380
+ Write: "write",
60381
+ Edit: "edit",
60382
+ Grep: "grep",
60383
+ Glob: "glob"
60384
+ };
60385
+ const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_TOOL_NAMES).map(([canonical, tool]) => [tool, canonical]));
60386
+ const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60387
+ function isMatcherName(token) {
60388
+ return !token.startsWith("\\") && !token.startsWith("[");
60389
+ }
60390
+ function replaceMatcherNames(matcher, names) {
60391
+ return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60392
+ }
60393
+ function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
60394
+ const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60395
+ const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
60396
+ if (unmatched.length > 0) warn(`Cortex Code matches hook matchers case-sensitively against lowercase runtime tool names, and ${[...new Set(unmatched)].map((name) => quoteValueForWarning(name)).join(", ")} in the "${eventName}" matcher ${quoteValueForWarning(matcher)} is not a tool Cortex Code documents a runtime ID for, so it is written as-is and will likely never match. Replace it with the lowercase name Cortex Code uses for that tool, if it has one.`);
60397
+ return translated;
60398
+ }
60399
+ function cortexcodeToCanonicalMatcher(matcher) {
60400
+ return replaceMatcherNames(matcher, CORTEXCODE_TO_CANONICAL_TOOL_NAMES);
60401
+ }
60294
60402
  const CORTEXCODE_SPEC = {
60295
60403
  displayName: "Cortex Code",
60296
60404
  overrideKey: "cortexcode",
@@ -60300,7 +60408,7 @@ const CORTEXCODE_SPEC = {
60300
60408
  toolToCanonicalEventNames: CORTEXCODE_TO_CANONICAL_EVENT_NAMES,
60301
60409
  projectDirVar: "$CORTEX_PROJECT_DIR",
60302
60410
  prefixDotRelativeCommandsOnly: true,
60303
- noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
60411
+ noMatcherEvents: CORTEXCODE_NO_MATCHER_EVENTS,
60304
60412
  supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
60305
60413
  booleanPassthroughFields: [{
60306
60414
  canonical: "enabled",
@@ -60315,7 +60423,11 @@ const CORTEXCODE_SPEC = {
60315
60423
  canonical: "source",
60316
60424
  tool: "source",
60317
60425
  commandOnly: true
60318
- }]
60426
+ }],
60427
+ matcherTranslation: {
60428
+ toTool: canonicalToCortexcodeMatcher,
60429
+ toCanonical: cortexcodeToCanonicalMatcher
60430
+ }
60319
60431
  }
60320
60432
  };
60321
60433
  /**
@@ -60496,35 +60608,80 @@ const NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entr
60496
60608
  function normalizeCrushEventName(event) {
60497
60609
  return event.replaceAll("_", "").toLowerCase();
60498
60610
  }
60611
+ /** Crush tool events, by normalized spelling, on which a `matcher` is valid. */
60612
+ const NORMALIZED_CRUSH_TOOL_EVENTS = new Set(CRUSH_TOOL_EVENTS.map((event) => normalizeCrushEventName(event)));
60613
+ /**
60614
+ * Resolve a `crush.hooks` override key to the canonical event it names, or
60615
+ * null when it names no canonical event. The key may be spelled the canonical
60616
+ * way (`stop`) or in any spelling Crush accepts for the event (`TurnEnd`,
60617
+ * `turn_end`), so an override an earlier import filed under a Crush name still
60618
+ * lands on the same event as the shared hooks.
60619
+ */
60620
+ function resolveOverrideCanonicalEvent(key) {
60621
+ if (lookupOwn({
60622
+ record: CANONICAL_TO_CRUSH_EVENT_NAMES,
60623
+ key
60624
+ }) !== void 0) return key;
60625
+ return lookupOwn({
60626
+ record: NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES,
60627
+ key: normalizeCrushEventName(key)
60628
+ }) ?? null;
60629
+ }
60499
60630
  /**
60500
60631
  * Build the `hooks` block of `crush.json` from a canonical hooks config.
60501
60632
  * Crush keys a flat array of `{name, matcher, command, timeout}` entries by
60502
60633
  * event name; `matcher` is a regex tested against the (lower-case) Crush tool
60503
60634
  * name and `timeout` is in seconds. Only `type: "command"` canonical hooks are
60504
60635
  * emitted, since a Crush hook is a shell command. A shared canonical event
60505
- * Crush does not fire is skipped (the HooksProcessor reports it), while an
60506
- * event under the `crush.hooks` override — such as one an import filed there
60507
- * — is written verbatim so it round-trips.
60636
+ * Crush does not fire is skipped (the HooksProcessor reports it).
60637
+ *
60638
+ * Every `crush.hooks` override key is resolved to its event first, whatever
60639
+ * spelling it uses: the override replaces the shared entries of that event
60640
+ * (as for every other tool's override), and override keys that name the same
60641
+ * event are concatenated in order, the way Crush itself merges them. A key
60642
+ * that names no canonical event — such as `PermissionResult`, or one an
60643
+ * import filed there — is written verbatim so it round-trips; keys of that
60644
+ * kind that Crush reads as the same event are concatenated under the first
60645
+ * spelling. A `matcher` is only written on a tool event: Crush rejects the
60646
+ * whole config when one appears anywhere else.
60508
60647
  */
60509
60648
  function canonicalToCrushHooks({ config, toolOverride, logger }) {
60510
- const sharedHooks = {};
60511
- for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_CRUSH_EVENTS.has(event)) sharedHooks[event] = defs;
60512
- const effective = {
60513
- ...sharedHooks,
60514
- ...toolOverride
60515
- };
60516
- const hooks = {};
60517
- for (const [event, defs] of Object.entries(effective)) {
60518
- if (isPrototypePollutionKey(event)) continue;
60519
- const crushEvent = lookupOwn({
60649
+ const shared = /* @__PURE__ */ new Map();
60650
+ for (const [event, defs] of Object.entries(config.hooks)) {
60651
+ const crushEvent = SUPPORTED_CRUSH_EVENTS.has(event) ? lookupOwn({
60520
60652
  record: CANONICAL_TO_CRUSH_EVENT_NAMES,
60521
60653
  key: event
60522
- }) ?? event;
60654
+ }) : void 0;
60655
+ if (crushEvent !== void 0) shared.set(crushEvent, defs);
60656
+ }
60657
+ const override = /* @__PURE__ */ new Map();
60658
+ const verbatimSpellings = /* @__PURE__ */ new Map();
60659
+ for (const [key, defs] of Object.entries(toolOverride ?? {})) {
60660
+ if (isPrototypePollutionKey(key) || !Array.isArray(defs)) continue;
60661
+ const canonical = resolveOverrideCanonicalEvent(key);
60662
+ let crushEvent;
60663
+ if (canonical !== null) crushEvent = lookupOwn({
60664
+ record: CANONICAL_TO_CRUSH_EVENT_NAMES,
60665
+ key: canonical
60666
+ }) ?? key;
60667
+ else {
60668
+ const normalized = normalizeCrushEventName(key);
60669
+ crushEvent = verbatimSpellings.get(normalized) ?? key;
60670
+ verbatimSpellings.set(normalized, crushEvent);
60671
+ }
60672
+ override.set(crushEvent, [...override.get(crushEvent) ?? [], ...defs]);
60673
+ }
60674
+ const effective = new Map(shared);
60675
+ for (const [crushEvent, defs] of override) effective.set(crushEvent, defs);
60676
+ const hooks = {};
60677
+ for (const [crushEvent, defs] of effective) {
60678
+ const allowsMatcher = NORMALIZED_CRUSH_TOOL_EVENTS.has(normalizeCrushEventName(crushEvent));
60523
60679
  const entries = [];
60524
60680
  for (const def of defs) {
60525
60681
  const entry = canonicalDefToCrushEntry({
60526
60682
  def,
60527
- event,
60683
+ event: crushEvent,
60684
+ allowsMatcher,
60528
60685
  logger
60529
60686
  });
60530
60687
  if (entry !== null) entries.push(entry);
@@ -60534,7 +60691,7 @@ function canonicalToCrushHooks({ config, toolOverride, logger }) {
60534
60691
  return hooks;
60535
60692
  }
60536
60693
  /** Convert one canonical hook definition to a Crush entry, or null to skip. */
60537
- function canonicalDefToCrushEntry({ def, event, logger }) {
60694
+ function canonicalDefToCrushEntry({ def, event, allowsMatcher, logger }) {
60538
60695
  if ((def.type ?? "command") !== "command") return null;
60539
60696
  if (typeof def.command !== "string" || def.command === "") {
60540
60697
  logger?.warn(`Crush hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Crush would discard, so it was skipped.`);
@@ -60542,7 +60699,10 @@ function canonicalDefToCrushEntry({ def, event, logger }) {
60542
60699
  }
60543
60700
  const entry = { command: def.command };
60544
60701
  if (typeof def.name === "string" && def.name !== "") entry.name = def.name;
60545
- if (typeof def.matcher === "string" && def.matcher !== "" && def.matcher !== "*") entry.matcher = def.matcher;
60702
+ if (typeof def.matcher === "string" && def.matcher !== "" && def.matcher !== "*") {
60703
+ if (allowsMatcher) entry.matcher = def.matcher;
60704
+ else logger?.warn(`matcher ${quoteValueForWarning(def.matcher)} on "${event}" hook will be ignored — Crush only accepts a matcher on tool events.`);
60705
+ }
60546
60706
  if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
60547
60707
  return entry;
60548
60708
  }
@@ -60589,12 +60749,13 @@ function crushHooksToCanonical(hooksBlock) {
60589
60749
  * `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
60590
60750
  * lists concatenated) at project scope and `~/.config/crush/crush.json` at
60591
60751
  * user scope — as
60592
- * `hooks.<Event>: [{name?, matcher?, command, timeout?}]`. Only `PreToolUse`
60593
- * fires today. The `hooks` key is owned outright; every other top-level key
60752
+ * `hooks.<Event>: [{name?, matcher?, command, timeout?}]`. `matcher` is only
60753
+ * valid on tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`,
60754
+ * `PermissionRequest`, `PermissionResult`). The `hooks` key is owned outright; every other top-level key
60594
60755
  * of the file is preserved and the file is never deleted.
60595
60756
  *
60596
- * @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
60597
- * @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
60757
+ * @see https://github.com/charmbracelet/crush/blob/v0.99.0/docs/hooks/README.md
60758
+ * @see https://github.com/charmbracelet/crush/blob/v0.99.0/internal/config/hooks.go
60598
60759
  */
60599
60760
  var CrushHooks = class CrushHooks extends ToolHooks {
60600
60761
  json;
@@ -63440,8 +63601,11 @@ var LettacodeHooks = class LettacodeHooks extends ToolHooks {
63440
63601
  * OpenCode 2.x no longer runs V1 plugin modules: a plugin must default-export
63441
63602
  * `{ id, setup(ctx) }`. Inside `setup` hooks are registered per domain
63442
63603
  * (`await ctx.tool.hook("execute.before", handler)`), and lifecycle events
63443
- * are read from `ctx.event.subscribe({ signal })`, an async iterable of the
63444
- * public event stream whose payload lives under `event.data`.
63604
+ * are read from `ctx.event.subscribe({ types, signal })`, an async iterable of
63605
+ * the public event stream whose payload lives under `event.data`. `types`
63606
+ * narrows the stream to the event types the plugin handles (a newer 2.x
63607
+ * option; the per-type checks inside the loop stay, so an older 2.x that
63608
+ * ignores it still runs each command only for its own event).
63445
63609
  *
63446
63610
  * Commands run through a private Bun Shell instance (`new Bun.$.Shell()`, the
63447
63611
  * plugin runs inside the Bun-built OpenCode binary) whose working directory is
@@ -63544,13 +63708,18 @@ function buildHookRegistrationLines(hookPoint, handlers) {
63544
63708
  }
63545
63709
  /**
63546
63710
  * Emit the event-stream loop. Each command is isolated in its own try/catch so
63547
- * a failing hook neither skips its siblings nor ends the subscription.
63711
+ * a failing hook neither skips its siblings nor ends the subscription. The
63712
+ * subscription passes `types` so the server delivers only the handled events.
63548
63713
  */
63549
63714
  function buildEventLoopLines(eventHandlers) {
63550
63715
  const lines = [];
63716
+ const types = [...eventHandlers.keys()].map((eventType) => JSON.stringify(eventType)).join(", ");
63551
63717
  lines.push("const __rulesyncEvents = new AbortController();");
63552
63718
  lines.push("void (async () => {");
63553
- lines.push(" for await (const event of ctx.event.subscribe({ signal: __rulesyncEvents.signal })) {");
63719
+ lines.push(" for await (const event of ctx.event.subscribe({");
63720
+ lines.push(` types: [${types}],`);
63721
+ lines.push(" signal: __rulesyncEvents.signal,");
63722
+ lines.push(" })) {");
63554
63723
  let isFirst = true;
63555
63724
  for (const [eventType, handlers] of eventHandlers) {
63556
63725
  lines.push(` ${isFirst ? "if" : "} else if"} (event.type === "${eventType}") {`);
@@ -65684,7 +65853,12 @@ var VibeHooks = class extends ToolHooks {
65684
65853
  * resolves inside the bundle and is written as is. The file is loaded only
65685
65854
  * when `plugin.json` declares the `ai.mistral.vibe` extension.
65686
65855
  *
65856
+ * Plugin hooks run on Vibe v2.x and on v3.0.4+. v3.0.0 through v3.0.3 do not
65857
+ * run them (v3.0.4 Fixed: "Hooks shipped in a native Vibe plugin's
65858
+ * `ai.mistral.vibe/hooks.toml` now run"). The output is the same either way.
65859
+ *
65687
65860
  * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.8/vibe/core/plugins/_native.py
65861
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.4/CHANGELOG.md
65688
65862
  */
65689
65863
  var VibePluginHooks = class extends VibeHooks {
65690
65864
  static getSettablePaths() {
@@ -65768,6 +65942,14 @@ const ZCODE_CONVERTER_CONFIG = {
65768
65942
  tool: "shell",
65769
65943
  commandOnly: true
65770
65944
  }],
65945
+ keywordPassthroughFields: [{
65946
+ canonical: "failClosed",
65947
+ tool: "onError",
65948
+ trueValue: "deny",
65949
+ falseValue: "allow",
65950
+ events: /* @__PURE__ */ new Set(["preToolUse"]),
65951
+ ignoredWhenFlags: ["async"]
65952
+ }],
65771
65953
  wildcardMatcherMeansAll: true
65772
65954
  };
65773
65955
  /**
@@ -73908,9 +74090,21 @@ function toGitlabduoServer({ serverName, serverConfig, logger }) {
73908
74090
  ...rest
73909
74091
  };
73910
74092
  }
74093
+ /**
74094
+ * GitLab Duo CLI reserves the MCP server name `duo` for its first-party server
74095
+ * (gitlab-lsp v9.33.0+): a workspace, user or plugin entry named exactly `duo`
74096
+ * (case-sensitive) is dropped with only a log line, so it would never load.
74097
+ *
74098
+ * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/commit/0572967c915465c1ac94ca38ef57676eb44e16d6
74099
+ */
74100
+ const GITLABDUO_RESERVED_MCP_SERVER_NAME = "duo";
73911
74101
  function toGitlabduoServers(servers, logger) {
73912
74102
  const result = {};
73913
74103
  for (const [serverName, serverConfig] of Object.entries(servers)) {
74104
+ if (serverName === GITLABDUO_RESERVED_MCP_SERVER_NAME) {
74105
+ logger?.warn(`GitLab Duo CLI MCP: skipping "${serverName}" because GitLab Duo CLI 9.33.0+ reserves that name for its first-party server and never loads a config entry with it. Rename the server to use it with GitLab Duo CLI.`);
74106
+ continue;
74107
+ }
73914
74108
  const converted = toGitlabduoServer({
73915
74109
  serverName,
73916
74110
  serverConfig,
@@ -75156,24 +75350,47 @@ async function readKimiCodeConfig({ outputRoot }) {
75156
75350
  }
75157
75351
  }
75158
75352
  /**
75353
+ * Kimi's reconnect delays are millisecond counts, so only a non-negative safe
75354
+ * integer is passed through. Anything else (a negative, a fraction, a string)
75355
+ * is dropped with a warning rather than written into, or lifted out of, the
75356
+ * shared global `config.toml`.
75357
+ */
75358
+ function toKimiCodeReconnectDelay({ value, key, logger }) {
75359
+ if (value === void 0) return;
75360
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
75361
+ warnWithFallback(logger, `Kimi Code MCP: dropping ${key} because it is not a non-negative integer number of milliseconds.`);
75362
+ }
75363
+ /**
75159
75364
  * Read the `[mcp]` defaults back out of the shared user `config.toml` so a
75160
75365
  * generate → import round trip keeps them. Absent or unparseable content yields
75161
75366
  * no defaults rather than failing the import: this file belongs to three
75162
75367
  * features plus the user.
75163
75368
  */
75164
- async function readKimiCodeMcpDefaults({ outputRoot }) {
75369
+ async function readKimiCodeMcpDefaults({ outputRoot, logger }) {
75165
75370
  const config = await readKimiCodeConfig({ outputRoot });
75166
75371
  if (!config.parsed) return {};
75372
+ const reconnectInitialDelayMs = toKimiCodeReconnectDelay({
75373
+ value: config.mcp.reconnect_initial_delay_ms,
75374
+ key: "[mcp] reconnect_initial_delay_ms",
75375
+ logger
75376
+ });
75377
+ const reconnectMaxDelayMs = toKimiCodeReconnectDelay({
75378
+ value: config.mcp.reconnect_max_delay_ms,
75379
+ key: "[mcp] reconnect_max_delay_ms",
75380
+ logger
75381
+ });
75167
75382
  return {
75168
75383
  ...typeof config.mcp.startup_timeout_ms === "number" && { startupTimeoutMs: config.mcp.startup_timeout_ms },
75169
75384
  ...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms },
75170
- ...typeof config.mcp.default_deferred === "boolean" && { defaultDeferred: config.mcp.default_deferred }
75385
+ ...typeof config.mcp.default_deferred === "boolean" && { defaultDeferred: config.mcp.default_deferred },
75386
+ ...reconnectInitialDelayMs !== void 0 && { reconnectInitialDelayMs },
75387
+ ...reconnectMaxDelayMs !== void 0 && { reconnectMaxDelayMs }
75171
75388
  };
75172
75389
  }
75173
75390
  /**
75174
75391
  * Kimi's `[mcp]` section in the shared user config sets the default connect and
75175
- * per-tool-call timeouts, and whether tools load on demand, for *every* MCP
75176
- * server, including ones rulesync did not write. Per-server values in
75392
+ * per-tool-call timeouts, whether tools load on demand, and the automatic
75393
+ * reconnect backoff for *every* MCP server, including ones rulesync did not write. Per-server values in
75177
75394
  * `mcp.json` still take precedence; this only moves the default.
75178
75395
  *
75179
75396
  * Written as an auxiliary file so it goes through the normal write phase and
@@ -75211,7 +75428,9 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
75211
75428
  ...existingSection,
75212
75429
  ...defaults.startupTimeoutMs !== void 0 && { startup_timeout_ms: defaults.startupTimeoutMs },
75213
75430
  ...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs },
75214
- ...defaults.defaultDeferred !== void 0 && { default_deferred: defaults.defaultDeferred }
75431
+ ...defaults.defaultDeferred !== void 0 && { default_deferred: defaults.defaultDeferred },
75432
+ ...defaults.reconnectInitialDelayMs !== void 0 && { reconnect_initial_delay_ms: defaults.reconnectInitialDelayMs },
75433
+ ...defaults.reconnectMaxDelayMs !== void 0 && { reconnect_max_delay_ms: defaults.reconnectMaxDelayMs }
75215
75434
  } },
75216
75435
  filePath: relativeConfigPath
75217
75436
  });
@@ -75260,7 +75479,10 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
75260
75479
  fileContent,
75261
75480
  validate,
75262
75481
  global,
75263
- configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {},
75482
+ configDefaults: global ? await readKimiCodeMcpDefaults({
75483
+ outputRoot,
75484
+ logger
75485
+ }) : {},
75264
75486
  logger
75265
75487
  });
75266
75488
  }
@@ -75310,7 +75532,17 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
75310
75532
  const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
75311
75533
  const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
75312
75534
  const defaultDeferred = typeof block.defaultDeferred === "boolean" ? block.defaultDeferred : void 0;
75313
- if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0 && defaultDeferred === void 0) return [];
75535
+ const reconnectInitialDelayMs = toKimiCodeReconnectDelay({
75536
+ value: block.reconnectInitialDelayMs,
75537
+ key: "kimi-code.reconnectInitialDelayMs",
75538
+ logger
75539
+ });
75540
+ const reconnectMaxDelayMs = toKimiCodeReconnectDelay({
75541
+ value: block.reconnectMaxDelayMs,
75542
+ key: "kimi-code.reconnectMaxDelayMs",
75543
+ logger
75544
+ });
75545
+ if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0 && defaultDeferred === void 0 && reconnectInitialDelayMs === void 0 && reconnectMaxDelayMs === void 0) return [];
75314
75546
  const existing = await readKimiCodeConfig({ outputRoot });
75315
75547
  if (!existing.parsed) {
75316
75548
  const paths = kimiCodeConfigRelativePath();
@@ -75322,7 +75554,9 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
75322
75554
  defaults: {
75323
75555
  startupTimeoutMs,
75324
75556
  toolTimeoutMs,
75325
- defaultDeferred
75557
+ defaultDeferred,
75558
+ reconnectInitialDelayMs,
75559
+ reconnectMaxDelayMs
75326
75560
  },
75327
75561
  existing
75328
75562
  })];
@@ -85673,6 +85907,7 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
85673
85907
  "CLAUDE_CODE_DISABLE_MOUNTED_DRIVE_RM_PROMPT",
85674
85908
  "CLAUDE_CODE_DISABLE_POWERSHELL_CMD_RM_DENY",
85675
85909
  "CLAUDE_CODE_DISABLE_SUBSTITUTION_RM_PROMPT",
85910
+ "CLAUDE_CODE_EVEN_MORE_DANGEROUSLY_SKIP_PERMISSIONS",
85676
85911
  "CLAUDE_CODE_MESSAGING_SOCKET",
85677
85912
  "CLAUDE_CODE_MESSAGING_TOKEN",
85678
85913
  "CLAUDE_CODE_PROJECT_DIR_NAME",
@@ -104918,32 +105153,59 @@ function buildChecksStrategy(ctx) {
104918
105153
  }
104919
105154
  // src/features/shared/antigravity-plugin-manifest.ts
104920
105155
  const ANTIGRAVITY_PLUGIN_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/;
105156
+ const ANTIGRAVITY_PLUGIN_STRICT_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
105157
+ /**
105158
+ * Lower-case a directory name and turn `_` into `-` (collapsing repeated and
105159
+ * trimming edge hyphens), so `My_Plugin` becomes `my-plugin`.
105160
+ */
105161
+ function toStrictAntigravityPluginName(name) {
105162
+ return name.toLowerCase().replace(/[-_]+/g, "-").replace(/^-+|-+$/g, "");
105163
+ }
104921
105164
  /**
104922
105165
  * Create the `plugin.json` manifest Antigravity requires before it treats a
104923
105166
  * directory as a plugin, so a tree generated from scratch actually loads.
104924
105167
  *
104925
- * The manifest declares only `name` and `description`, and rejects anything
104926
- * else, so the one required field is derived from the plugin directory's name.
104927
- * An existing manifest is never read or rewritten: it may carry a
104928
- * hand-authored `name` or `description` that rulesync has no source for. A
105168
+ * Only the required `name` is written, derived from the plugin directory's
105169
+ * name and normalized to the lowercase kebab-case form `agy plugin validate
105170
+ * --strict` and the Marketplace require; the other manifest fields (such as
105171
+ * `description`, `version` or `author`) have no rulesync source. An existing
105172
+ * manifest is never rewritten: it may carry hand-authored fields. Its `name` is
105173
+ * read only to report a collision with another output root's name. A
104929
105174
  * manifest kept in one of the manifest folders counts as present too, so no
104930
105175
  * root stub is written next to it.
104931
105176
  */
104932
105177
  async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger }) {
104933
105178
  const paths = [];
105179
+ const seenNames = /* @__PURE__ */ new Map();
105180
+ const noteName = (name, manifestPath) => {
105181
+ const previousPath = seenNames.get(name);
105182
+ if (previousPath === void 0) {
105183
+ seenNames.set(name, manifestPath);
105184
+ return;
105185
+ }
105186
+ warnOnceWithFallback(logger, `The Antigravity plugin manifests ${quoteForLog(manifestPath)} and ${quoteForLog(previousPath)} both name the plugin ${quoteForLog(name)}, so the plugins collide in Antigravity. Rename one of the directories or set a distinct "name" by hand.`);
105187
+ };
104934
105188
  for (const outputRoot of outputRoots) {
104935
105189
  const filePath = (0, node_path.join)(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME);
104936
- if (await hasAntigravityPluginManifest(outputRoot)) continue;
105190
+ const existingManifestPath = await findAntigravityPluginManifest(outputRoot);
105191
+ if (existingManifestPath !== void 0) {
105192
+ const existingName = await readManifestName(existingManifestPath);
105193
+ if (existingName !== void 0) noteName(existingName, existingManifestPath);
105194
+ continue;
105195
+ }
104937
105196
  if (await refusesWriteOutsideRoot({
104938
105197
  logger,
104939
105198
  rootPath: outputRoot,
104940
105199
  targetPath: filePath
104941
105200
  })) continue;
104942
- const name = (0, node_path.basename)((0, node_path.resolve)(outputRoot));
104943
- if (!ANTIGRAVITY_PLUGIN_NAME_PATTERN.test(name)) {
104944
- warnOnceWithFallback(logger, `Cannot derive an Antigravity plugin name from the directory ${quoteForLog(name)}: it must match ${ANTIGRAVITY_PLUGIN_NAME_PATTERN.source}. Create ${quoteForLog(filePath)} by hand with a valid "name".`);
105201
+ const dirName = (0, node_path.basename)((0, node_path.resolve)(outputRoot));
105202
+ const name = toStrictAntigravityPluginName(dirName);
105203
+ if (!ANTIGRAVITY_PLUGIN_NAME_PATTERN.test(dirName) || !ANTIGRAVITY_PLUGIN_STRICT_NAME_PATTERN.test(name)) {
105204
+ warnOnceWithFallback(logger, `Cannot derive an Antigravity plugin name from the directory ${quoteForLog(dirName)}: it must contain only letters, digits, "-" and "_", and at least one letter or digit, so that it normalizes to ${ANTIGRAVITY_PLUGIN_STRICT_NAME_PATTERN.source}. Create ${quoteForLog(filePath)} by hand with a valid "name".`);
104945
105205
  continue;
104946
105206
  }
105207
+ if (name !== dirName) warnOnceWithFallback(logger, `The Antigravity plugin directory ${quoteForLog(dirName)} is not lowercase kebab-case, which \`agy plugin validate --strict\` and the Marketplace reject, so ${quoteForLog(filePath)} names the plugin ${quoteForLog(name)}.`);
105208
+ noteName(name, filePath);
104947
105209
  const content = addTrailingNewline(JSON.stringify({ name }, null, 2));
104948
105210
  if (dryRun) logger.info(`[DRY RUN] Would write: ${stripControlCharacters(filePath)}`);
104949
105211
  else await writeFileContent(filePath, content);
@@ -104956,10 +105218,18 @@ async function ensureAntigravityPluginManifests({ outputRoots, dryRun, logger })
104956
105218
  sourceLoadFailed: false
104957
105219
  };
104958
105220
  }
104959
- async function hasAntigravityPluginManifest(outputRoot) {
105221
+ async function findAntigravityPluginManifest(outputRoot) {
104960
105222
  const candidates = [(0, node_path.join)(outputRoot, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME), ...ANTIGRAVITY_PLUGIN_MANIFEST_FOLDERS.map((folder) => (0, node_path.join)(outputRoot, folder, ANTIGRAVITY_PLUGIN_MANIFEST_FILE_NAME))];
104961
- for (const candidate of candidates) if (await fileExists(candidate)) return true;
104962
- return false;
105223
+ for (const candidate of candidates) if (await fileExists(candidate)) return candidate;
105224
+ }
105225
+ /** The manifest's string `name`, or `undefined` when it is unreadable or absent. */
105226
+ async function readManifestName(manifestPath) {
105227
+ try {
105228
+ const parsed = JSON.parse(await readFileContent(manifestPath));
105229
+ return isRecord(parsed) && typeof parsed.name === "string" ? parsed.name : void 0;
105230
+ } catch {
105231
+ return;
105232
+ }
104963
105233
  }
104964
105234
  // src/features/shared/hermes-project-plugin-activation.ts
104965
105235
  function mergeEnabledPlugins({ existingContent, filePath, pluginNames }) {