rulesync 16.7.0 → 16.8.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.
@@ -1867,7 +1867,7 @@ var RulesyncFile = class extends AiFile {
1867
1867
  * Type guard to check if a value is a plain object (Record<string, unknown>).
1868
1868
  * This excludes arrays and null values.
1869
1869
  */
1870
- function isRecord(value) {
1870
+ function isRecord$1(value) {
1871
1871
  return typeof value === "object" && value !== null && !Array.isArray(value);
1872
1872
  }
1873
1873
  /**
@@ -1882,7 +1882,7 @@ function isRecord(value) {
1882
1882
  * malicious accessor descriptors.
1883
1883
  */
1884
1884
  function isPlainObject$1(value) {
1885
- if (!isRecord(value)) return false;
1885
+ if (!isRecord$1(value)) return false;
1886
1886
  const proto = Object.getPrototypeOf(value);
1887
1887
  return proto === null || proto === Object.prototype;
1888
1888
  }
@@ -2188,6 +2188,7 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
2188
2188
  timeout: zod_mini.z.optional(zod_mini.z.number()),
2189
2189
  cacheTtl: zod_mini.z.optional(zod_mini.z.number().check((0, zod_mini.nonnegative)())),
2190
2190
  matcher: zod_mini.z.optional(safeString),
2191
+ enabled: zod_mini.z.optional(zod_mini.z.boolean()),
2191
2192
  prompt: zod_mini.z.optional(safeString),
2192
2193
  loop_limit: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.number())),
2193
2194
  name: zod_mini.z.optional(safeString),
@@ -2506,8 +2507,9 @@ const FACTORYDROID_HOOK_EVENTS = [
2506
2507
  /**
2507
2508
  * Hook events supported by deepagents-cli (`deepagents-code` / `dcode`).
2508
2509
  *
2509
- * The canonical `notification` event maps to dcode's `input.required`
2510
- * (human-in-the-loop interrupt) the closest documented equivalent.
2510
+ * These are the twelve Hooks v2 `HookEvent` members, GA since deepagents-code
2511
+ * 0.1.52. Canonical `contextOffload` is deliberately absent — see
2512
+ * {@link CANONICAL_TO_DEEPAGENTS_EVENT_NAMES}.
2511
2513
  * https://docs.langchain.com/oss/python/deepagents/cli/configuration
2512
2514
  */
2513
2515
  const DEEPAGENTS_HOOK_EVENTS = [
@@ -2520,8 +2522,9 @@ const DEEPAGENTS_HOOK_EVENTS = [
2520
2522
  "postToolUseFailure",
2521
2523
  "stop",
2522
2524
  "preCompact",
2523
- "contextOffload",
2524
- "notification"
2525
+ "notification",
2526
+ "subagentStart",
2527
+ "subagentStop"
2525
2528
  ];
2526
2529
  /** Hook events supported by Codex CLI. */
2527
2530
  const CODEXCLI_HOOK_EVENTS = [
@@ -3209,26 +3212,59 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
3209
3212
  */
3210
3213
  const GOOSE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GOOSE_EVENT_NAMES).map(([k, v]) => [v, k]));
3211
3214
  /**
3212
- * Map canonical camelCase event names to deepagents-cli dot-notation.
3215
+ * Map canonical camelCase event names to the deepagents-cli Hooks v2
3216
+ * `HookEvent` values.
3217
+ *
3218
+ * Hooks v2 went GA in deepagents-code 0.1.52 (2026-08-04) and replaced the
3219
+ * legacy dot-notation names (`session.start`, `tool.use`, …) with these twelve
3220
+ * PascalCase members. The legacy list format is still read, but is scheduled for
3221
+ * removal on 2026-09-01 (`_LEGACY_HOOKS_REMOVAL_DATE` in `hooks/loading.py`).
3222
+ *
3223
+ * Canonical `contextOffload` has no v2 counterpart. Its legacy event
3224
+ * (`context.offload`) is gone, and folding it onto `PreCompact` would silently
3225
+ * merge two distinct canonical events into one — so it is dropped for
3226
+ * deepagents instead, and reported by the hooks processor as an unsupported
3227
+ * event like any other.
3228
+ *
3229
+ * @see https://github.com/langchain-ai/deepagents `libs/code/deepagents_code/hooks/models/domain.py`
3213
3230
  */
3214
3231
  const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
3215
- sessionStart: "session.start",
3216
- sessionEnd: "session.end",
3217
- beforeSubmitPrompt: "user.prompt",
3218
- permissionRequest: "permission.request",
3219
- preToolUse: "tool.use",
3220
- postToolUse: "tool.result",
3221
- postToolUseFailure: "tool.error",
3222
- stop: "task.complete",
3223
- preCompact: "context.compact",
3224
- contextOffload: "context.offload",
3225
- notification: "input.required"
3232
+ sessionStart: "SessionStart",
3233
+ beforeSubmitPrompt: "UserPromptSubmit",
3234
+ sessionEnd: "SessionEnd",
3235
+ permissionRequest: "PermissionRequest",
3236
+ notification: "Notification",
3237
+ preToolUse: "PreToolUse",
3238
+ postToolUse: "PostToolUse",
3239
+ postToolUseFailure: "PostToolUseFailure",
3240
+ preCompact: "PreCompact",
3241
+ stop: "Stop",
3242
+ subagentStart: "SubagentStart",
3243
+ subagentStop: "SubagentStop"
3226
3244
  };
3227
3245
  /**
3228
- * Map deepagents-cli dot-notation event names to canonical camelCase.
3246
+ * Map deepagents-cli `HookEvent` values to canonical camelCase.
3229
3247
  */
3230
3248
  const DEEPAGENTS_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEEPAGENTS_EVENT_NAMES).map(([k, v]) => [v, k]));
3231
3249
  /**
3250
+ * The legacy dot-notation event names deepagents-cli used before Hooks v2.
3251
+ * Kept for the read-only import path so a `hooks.json` still in the old format
3252
+ * round-trips into canonical events instead of being silently discarded.
3253
+ */
3254
+ const DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES = {
3255
+ "session.start": "sessionStart",
3256
+ "session.end": "sessionEnd",
3257
+ "user.prompt": "beforeSubmitPrompt",
3258
+ "permission.request": "permissionRequest",
3259
+ "tool.use": "preToolUse",
3260
+ "tool.result": "postToolUse",
3261
+ "tool.error": "postToolUseFailure",
3262
+ "task.complete": "stop",
3263
+ "context.compact": "preCompact",
3264
+ "context.offload": "contextOffload",
3265
+ "input.required": "notification"
3266
+ };
3267
+ /**
3232
3268
  * Map canonical camelCase event names to Kiro CLI camelCase.
3233
3269
  * Kiro CLI uses its own event naming: agentSpawn, userPromptSubmit, preToolUse,
3234
3270
  * postToolUse, stop. Both `sessionEnd` and `stop` canonical events map to
@@ -3846,7 +3882,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3846
3882
  logger
3847
3883
  });
3848
3884
  for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
3849
- if (!isRecord(json[ignoredKey])) continue;
3885
+ if (!isRecord$1(json[ignoredKey])) continue;
3850
3886
  this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
3851
3887
  }
3852
3888
  const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
@@ -3858,7 +3894,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3858
3894
  }));
3859
3895
  for (const blockKey of blockKeys) {
3860
3896
  const toolBlock = json[blockKey];
3861
- const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
3897
+ const toolServers = isRecord$1(toolBlock) && isRecord$1(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
3862
3898
  for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
3863
3899
  if (isPrototypePollutionKey(serverName)) continue;
3864
3900
  if (serverConfig === null) delete effectiveServers[serverName];
@@ -4875,9 +4911,9 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
4875
4911
  if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
4876
4912
  const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
4877
4913
  const json = this.json;
4878
- if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
4914
+ if (overrideKey !== toolTarget && isRecord$1(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
4879
4915
  const overrideBlock = json[overrideKey];
4880
- if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
4916
+ if (!isRecord$1(overrideBlock) || !isRecord$1(overrideBlock.permission)) return this;
4881
4917
  const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
4882
4918
  const merged = {
4883
4919
  ...json,
@@ -5187,6 +5223,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
5187
5223
  compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
5188
5224
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
5189
5225
  })),
5226
+ kiro: zod_mini.z.optional(zod_mini.z.looseObject({
5227
+ license: zod_mini.z.optional(zod_mini.z.string()),
5228
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
5229
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
5230
+ })),
5190
5231
  deepagents: zod_mini.z.optional(zod_mini.z.looseObject({
5191
5232
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
5192
5233
  license: zod_mini.z.optional(zod_mini.z.string()),
@@ -8281,7 +8322,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
8281
8322
  }
8282
8323
  static extractAntigravityConfig(rulesyncCommand) {
8283
8324
  const antigravity = rulesyncCommand.getFrontmatter().antigravity;
8284
- return isRecord(antigravity) ? antigravity : void 0;
8325
+ return isRecord$1(antigravity) ? antigravity : void 0;
8285
8326
  }
8286
8327
  static resolveTrigger(rulesyncCommand, antigravityConfig) {
8287
8328
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
@@ -12071,8 +12112,8 @@ async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
12071
12112
  }
12072
12113
  if (!isPlainObject$1(parsed) || !Array.isArray(parsed.prompts)) return "";
12073
12114
  const expectedContentFile = toPosixPath((0, node_path.join)("prompts", relativeFilePath));
12074
- const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
12075
- return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
12115
+ const entry = parsed.prompts.find((candidate) => isRecord$1(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
12116
+ return isRecord$1(entry) && typeof entry.description === "string" ? entry.description : "";
12076
12117
  }
12077
12118
  /**
12078
12119
  * The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
@@ -13185,97 +13226,140 @@ function applyCommandPrefix({ def, converterConfig }) {
13185
13226
  return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
13186
13227
  }
13187
13228
  /**
13188
- * Emit the configured boolean passthrough fields on the tool side, mapping each
13189
- * canonical field name to its (possibly renamed) tool field name. Only boolean
13190
- * values are carried through.
13191
- */
13192
- function emitBooleanPassthroughFields({ def, hookType, converterConfig }) {
13193
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13194
- if (commandOnly === true && hookType !== "command") return false;
13195
- return typeof def[canonical] === "boolean";
13196
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13197
- }
13198
- /**
13199
- * Import the configured boolean passthrough fields back into canonical fields,
13200
- * reversing {@link emitBooleanPassthroughFields}. Only boolean values are read.
13201
- */
13202
- function importBooleanPassthroughFields({ h, converterConfig }) {
13203
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "boolean").map(({ canonical, tool }) => [canonical, h[tool]]));
13204
- }
13205
- /**
13206
- * Emit the configured number passthrough fields on the tool side, mapping each
13207
- * canonical field name to its (possibly renamed) tool field name. Only finite
13208
- * numbers are carried through.
13209
- */
13210
- function emitNumberPassthroughFields({ def, hookType, converterConfig }) {
13211
- return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13212
- if (commandOnly === true && hookType !== "command") return false;
13213
- return Number.isFinite(def[canonical]);
13214
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13215
- }
13216
- /**
13217
- * Import the configured number passthrough fields back into canonical fields,
13218
- * reversing {@link emitNumberPassthroughFields}. Only finite numbers are read.
13219
- */
13220
- function importNumberPassthroughFields({ h, converterConfig }) {
13221
- return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ tool }) => Number.isFinite(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
13222
- }
13223
- /**
13224
- * Emit the configured string passthrough fields on the tool side, mapping each
13225
- * canonical field name to its (possibly renamed) tool field name. Only non-empty
13226
- * string values are carried through.
13227
- */
13228
- function emitStringPassthroughFields({ def, hookType, converterConfig }) {
13229
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13230
- if (commandOnly === true && hookType !== "command") return false;
13231
- return typeof def[canonical] === "string" && def[canonical] !== "";
13232
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13233
- }
13234
- /**
13235
- * Import the configured string passthrough fields back into canonical fields,
13236
- * reversing {@link emitStringPassthroughFields}. Only non-empty string values
13237
- * are read.
13238
- */
13239
- function importStringPassthroughFields({ h, converterConfig }) {
13240
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "string" && h[tool] !== "").map(({ canonical, tool }) => [canonical, h[tool]]));
13241
- }
13242
- /**
13243
- * Emit the configured string-array passthrough fields on the tool side.
13244
- */
13245
- function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
13246
- return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13247
- if (commandOnly === true && hookType !== "command") return false;
13248
- return isStringArray(def[canonical]);
13249
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13250
- }
13251
- /**
13252
- * Import the configured string-array passthrough fields, reversing
13253
- * {@link emitArrayPassthroughFields}.
13229
+ * Whether a field registered for `command` hooks only applies to this hook.
13230
+ * Applied on both export and import: a value imported into a canonical field
13231
+ * the exporter would then drop is silently deleted on the next generate.
13254
13232
  */
13255
- function importArrayPassthroughFields({ h, converterConfig, logger }) {
13256
- const fields = converterConfig.arrayPassthroughFields ?? [];
13257
- for (const { tool } of fields) if (h[tool] !== void 0 && !isSafeStringArray(h[tool])) logger?.warn(`Dropping "${tool}" while importing a hook: it must be a list of strings without newline, carriage return or NUL characters.`);
13258
- return Object.fromEntries(fields.filter(({ tool }) => isSafeStringArray(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
13233
+ function isFieldApplicable({ commandOnly, hookType }) {
13234
+ return commandOnly !== true || hookType === "command";
13259
13235
  }
13260
13236
  /**
13261
- * Emit the configured string-map passthrough fields on the tool side. Only maps
13262
- * whose values are all control-character-free strings are carried through.
13263
- */
13264
- function emitRecordPassthroughFields({ def, hookType, converterConfig }) {
13265
- return Object.fromEntries((converterConfig.recordPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13266
- if (commandOnly === true && hookType !== "command") return false;
13267
- return isSafeStringRecord(def[canonical]);
13268
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13269
- }
13270
- /**
13271
- * Import the configured string-map passthrough fields, reversing
13272
- * {@link emitRecordPassthroughFields}.
13237
+ * Emit the configured passthrough fields on the tool side, mapping each
13238
+ * canonical field name to its (possibly renamed) tool field name. Only values
13239
+ * accepted by `isValid` are carried through, so a malformed field can't leak
13240
+ * into a config the tool would reject.
13241
+ *
13242
+ * A field the tool documents on `command` hooks only is dropped when authored
13243
+ * on another hook type. That is a value the user hand-wrote in
13244
+ * `.rulesync/hooks.*` and the canonical schema does not cross-validate it
13245
+ * against the hook's type, so it is warned about rather than deleted in
13246
+ * silence — the mirror of the import side.
13273
13247
  */
13274
- function importRecordPassthroughFields({ h, hookType, converterConfig, logger }) {
13275
- const fields = (converterConfig.recordPassthroughFields ?? []).filter(({ commandOnly }) => commandOnly !== true || hookType === "command");
13276
- for (const { tool } of fields) if (h[tool] !== void 0 && !isSafeStringRecord(h[tool])) logger?.warn(`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.`);
13277
- return Object.fromEntries(fields.filter(({ tool }) => isSafeStringRecord(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
13278
- }
13248
+ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn }) {
13249
+ for (const { canonical, tool, commandOnly } of fields) {
13250
+ const value = def[canonical];
13251
+ if (value === void 0) continue;
13252
+ if (!isFieldApplicable({
13253
+ commandOnly,
13254
+ hookType
13255
+ })) {
13256
+ warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not generated.`);
13257
+ continue;
13258
+ }
13259
+ if (!isValid({
13260
+ value,
13261
+ canonical
13262
+ })) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${JSON.stringify(value)} is not a value this tool can express as "${tool}".`);
13263
+ }
13264
+ return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
13265
+ commandOnly,
13266
+ hookType
13267
+ }) && isValid({
13268
+ value: def[canonical],
13269
+ canonical
13270
+ })).map(({ canonical, tool }) => [tool, def[canonical]]));
13271
+ }
13272
+ /**
13273
+ * Import the configured passthrough fields back into canonical fields,
13274
+ * reversing {@link emitPassthroughFields}. A field the tool documents on
13275
+ * `command` hooks only is skipped here too, and `describeInvalid` — when the
13276
+ * kind has a rule an authored file can plausibly violate — turns a rejected
13277
+ * value into a warning instead of a silent drop.
13278
+ */
13279
+ function importPassthroughFields({ h, hookType, fields, isValid, describeInvalid, warn }) {
13280
+ const applicable = fields.filter(({ commandOnly }) => isFieldApplicable({
13281
+ commandOnly,
13282
+ hookType
13283
+ }));
13284
+ const skipped = fields.filter(({ commandOnly }) => !isFieldApplicable({
13285
+ commandOnly,
13286
+ hookType
13287
+ }));
13288
+ for (const { tool } of skipped) if (h[tool] !== void 0) warn?.(`Dropping "${tool}" from an imported "${hookType}" hook: this tool documents it on "command" hooks only, so it is not imported.`);
13289
+ for (const { tool, canonical } of applicable) if (describeInvalid !== void 0 && h[tool] !== void 0 && !isValid({
13290
+ value: h[tool],
13291
+ canonical
13292
+ })) warn?.(describeInvalid({
13293
+ tool,
13294
+ canonical,
13295
+ value: h[tool]
13296
+ }));
13297
+ return Object.fromEntries(applicable.filter(({ tool, canonical }) => isValid({
13298
+ value: h[tool],
13299
+ canonical
13300
+ })).map(({ canonical, tool }) => [canonical, h[tool]]));
13301
+ }
13302
+ const isBooleanValue = ({ value }) => typeof value === "boolean";
13303
+ const isFiniteNumber = ({ value }) => Number.isFinite(value);
13304
+ const isNonEmptyString = (value) => typeof value === "string" && value !== "";
13305
+ const isEmittableString = ({ value }) => isNonEmptyString(value);
13306
+ const isEmittableArray = ({ value }) => isStringArray(value);
13307
+ const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
13308
+ const isImportableArray = ({ value }) => isSafeStringArray(value);
13309
+ /**
13310
+ * The canonical schema of one hook field, looked up by name. Read off the
13311
+ * schema's own shape rather than by parsing a one-field object, so a `canonical`
13312
+ * name that no longer exists resolves to `undefined` (a `looseObject` would
13313
+ * accept an unknown key and silently validate nothing) and a typo is caught by
13314
+ * the tests instead of quietly disabling the check.
13315
+ */
13316
+ const CANONICAL_FIELD_SCHEMAS = HookDefinitionSchema.def.shape;
13317
+ /**
13318
+ * Whether a value satisfies the constraints the canonical schema puts on the
13319
+ * field it would be imported into.
13320
+ *
13321
+ * Import needs this on top of the kind's shape check: a hand-written tool
13322
+ * settings file can hold `"shell": "zsh"`, which is a non-empty string but not
13323
+ * a member of the canonical `z.enum(["bash", "powershell"])`. Letting it in
13324
+ * would write a `.rulesync/hooks.jsonc` that fails validation on the *next*
13325
+ * run, taking the whole hooks feature down with it.
13326
+ */
13327
+ function satisfiesCanonicalField({ value, canonical }) {
13328
+ const schema = canonicalFieldSchema(canonical);
13329
+ return schema !== void 0 && zod_mini.z.safeParse(schema, value).success;
13330
+ }
13331
+ /** Own properties only, so a name like `toString` resolves to nothing. */
13332
+ function canonicalFieldSchema(canonical) {
13333
+ return Object.hasOwn(CANONICAL_FIELD_SCHEMAS, canonical) ? CANONICAL_FIELD_SCHEMAS[canonical] : void 0;
13334
+ }
13335
+ const isImportableString = ({ value, canonical }) => isNonEmptyString(value) && satisfiesCanonicalField({
13336
+ value,
13337
+ canonical
13338
+ });
13339
+ const isImportableNumber = ({ value, canonical }) => Number.isFinite(value) && satisfiesCanonicalField({
13340
+ value,
13341
+ canonical
13342
+ });
13343
+ /**
13344
+ * Say which rule the value broke, so the warning names the actual constraint
13345
+ * rather than asserting a canonical rejection that may not be the reason. A
13346
+ * closed enum lists its members; a rule carrying its own message (the
13347
+ * control-character check behind `safeString`) reuses it.
13348
+ */
13349
+ function describeScalarConstraint({ canonical, value }) {
13350
+ const schema = canonicalFieldSchema(canonical);
13351
+ const result = schema === void 0 ? void 0 : zod_mini.z.safeParse(schema, value);
13352
+ if (result === void 0 || result.success) return `it is not a value this field carries through.`;
13353
+ const issue = result.error.issues[0];
13354
+ if (issue === void 0) return `it is not a value the canonical "${canonical}" field accepts.`;
13355
+ return `it does not satisfy the canonical "${canonical}" field: ${issue.message}.`;
13356
+ }
13357
+ const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${JSON.stringify(value)}) while importing a hook: ${describeScalarConstraint({
13358
+ canonical,
13359
+ value
13360
+ })} Importing it would fail validation on the next run.`;
13361
+ const describeInvalidArray = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a list of strings without newline, carriage return or NUL characters.`;
13362
+ 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.`;
13279
13363
  /**
13280
13364
  * Check a value against the shape its field documents. A string field also
13281
13365
  * rejects control characters, matching the canonical `safeString` so an
@@ -13336,7 +13420,54 @@ function emitTypePayloadFields({ def, hookType, converterConfig }) {
13336
13420
  function isSupportedHookType({ type, converterConfig }) {
13337
13421
  return converterConfig.supportedHookTypes?.has(type ?? "command") ?? true;
13338
13422
  }
13339
- function buildToolHooks({ defs, converterConfig }) {
13423
+ /**
13424
+ * Emit every per-hook passthrough kind for one canonical definition.
13425
+ */
13426
+ function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, warn }) {
13427
+ return {
13428
+ ...emitPassthroughFields({
13429
+ def,
13430
+ hookType,
13431
+ eventName,
13432
+ fields: converterConfig.booleanPassthroughFields ?? [],
13433
+ isValid: isBooleanValue,
13434
+ warn
13435
+ }),
13436
+ ...emitPassthroughFields({
13437
+ def,
13438
+ hookType,
13439
+ eventName,
13440
+ fields: converterConfig.numberPassthroughFields ?? [],
13441
+ isValid: isFiniteNumber,
13442
+ warn
13443
+ }),
13444
+ ...emitPassthroughFields({
13445
+ def,
13446
+ hookType,
13447
+ eventName,
13448
+ fields: converterConfig.stringPassthroughFields ?? [],
13449
+ isValid: isEmittableString,
13450
+ warn
13451
+ }),
13452
+ ...emitPassthroughFields({
13453
+ def,
13454
+ hookType,
13455
+ eventName,
13456
+ fields: converterConfig.arrayPassthroughFields ?? [],
13457
+ isValid: isEmittableArray,
13458
+ warn
13459
+ }),
13460
+ ...emitPassthroughFields({
13461
+ def,
13462
+ hookType,
13463
+ eventName,
13464
+ fields: converterConfig.recordPassthroughFields ?? [],
13465
+ isValid: isEmittableRecord,
13466
+ warn
13467
+ })
13468
+ };
13469
+ }
13470
+ function buildToolHooks({ defs, eventName, converterConfig, warn }) {
13340
13471
  const hooks = [];
13341
13472
  for (const def of defs) {
13342
13473
  const hookType = def.type ?? "command";
@@ -13349,30 +13480,12 @@ function buildToolHooks({ defs, converterConfig }) {
13349
13480
  converterConfig
13350
13481
  });
13351
13482
  hooks.push({
13352
- ...emitBooleanPassthroughFields({
13353
- def,
13354
- hookType,
13355
- converterConfig
13356
- }),
13357
- ...emitNumberPassthroughFields({
13358
- def,
13359
- hookType,
13360
- converterConfig
13361
- }),
13362
- ...emitStringPassthroughFields({
13483
+ ...emitAllPassthroughFields({
13363
13484
  def,
13364
13485
  hookType,
13365
- converterConfig
13366
- }),
13367
- ...emitArrayPassthroughFields({
13368
- def,
13369
- hookType,
13370
- converterConfig
13371
- }),
13372
- ...emitRecordPassthroughFields({
13373
- def,
13374
- hookType,
13375
- converterConfig
13486
+ eventName,
13487
+ converterConfig,
13488
+ warn
13376
13489
  }),
13377
13490
  type: hookType,
13378
13491
  ...command !== void 0 && command !== null && { command },
@@ -13390,6 +13503,17 @@ function buildToolHooks({ defs, converterConfig }) {
13390
13503
  return hooks;
13391
13504
  }
13392
13505
  /**
13506
+ * A `warn` that says each distinct thing once per conversion.
13507
+ */
13508
+ function warnOnce(logger) {
13509
+ const seen = /* @__PURE__ */ new Set();
13510
+ return (message) => {
13511
+ if (seen.has(message)) return;
13512
+ seen.add(message);
13513
+ logger?.warn(message);
13514
+ };
13515
+ }
13516
+ /**
13393
13517
  * Convert canonical hooks config to tool-specific format (shared by Claude and Factory Droid).
13394
13518
  * Uses explicit event name mapping tables rather than algorithmic case conversion,
13395
13519
  * since tool event names may differ entirely from canonical names
@@ -13401,6 +13525,7 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
13401
13525
  toolOverrideHooks,
13402
13526
  supportedEvents: converterConfig.supportedEvents
13403
13527
  });
13528
+ const warn = warnOnce(logger);
13404
13529
  const result = {};
13405
13530
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
13406
13531
  const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
@@ -13414,7 +13539,9 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
13414
13539
  if (isNoMatcherEvent && matcherKey) logger?.warn(`matcher "${matcherKey}" on "${eventName}" hook will be ignored — this event does not support matchers`);
13415
13540
  const hooks = buildToolHooks({
13416
13541
  defs,
13417
- converterConfig
13542
+ eventName,
13543
+ converterConfig,
13544
+ warn
13418
13545
  });
13419
13546
  if (hooks.length === 0) continue;
13420
13547
  const includeMatcher = matcherKey && !isNoMatcherEvent;
@@ -13520,85 +13647,283 @@ function isSafeStringArray(value) {
13520
13647
  return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
13521
13648
  }
13522
13649
  /**
13650
+ * A raw string kept only if the canonical field it would land in accepts it,
13651
+ * warning when it does not. The canonical string fields are guarded by
13652
+ * `safeString`, so an existing tool config carrying a control character would
13653
+ * otherwise be imported into a file the next generate refuses to read.
13654
+ */
13655
+ function importCanonicalString({ value, canonical, warn }) {
13656
+ if (typeof value !== "string") return;
13657
+ if (satisfiesCanonicalField({
13658
+ value,
13659
+ canonical
13660
+ })) return value;
13661
+ warn?.(describeInvalidScalar({
13662
+ tool: canonical,
13663
+ canonical,
13664
+ value
13665
+ }));
13666
+ }
13667
+ /**
13523
13668
  * Import the payload fields specific to a hook type, type-checking each raw
13524
13669
  * value before it enters the canonical definition.
13525
13670
  */
13526
- function importTypePayloadFields({ h, hookType }) {
13527
- if (hookType === "http") return {
13528
- ...typeof h.url === "string" && { url: h.url },
13529
- ...isStringRecord(h.headers) && { headers: h.headers },
13530
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
13531
- };
13532
- if (hookType === "mcp_tool") return {
13533
- ...typeof h.server === "string" && { server: h.server },
13534
- ...typeof h.tool === "string" && { tool: h.tool },
13535
- ...h.input !== null && typeof h.input === "object" && !Array.isArray(h.input) && { input: h.input }
13536
- };
13537
- if (hookType === "prompt" || hookType === "agent") return typeof h.model === "string" ? { model: h.model } : {};
13671
+ function importTypePayloadFields({ h, hookType, warn }) {
13672
+ if (hookType === "http") {
13673
+ const url = importCanonicalString({
13674
+ value: h.url,
13675
+ canonical: "url",
13676
+ warn
13677
+ });
13678
+ const headers = isStringRecord(h.headers) && !satisfiesCanonicalField({
13679
+ value: h.headers,
13680
+ canonical: "headers"
13681
+ }) ? (warn?.(describeInvalidScalar({
13682
+ tool: "headers",
13683
+ canonical: "headers",
13684
+ value: h.headers
13685
+ })), void 0) : h.headers;
13686
+ return {
13687
+ ...url !== void 0 && { url },
13688
+ ...isStringRecord(headers) && { headers },
13689
+ ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
13690
+ };
13691
+ }
13692
+ if (hookType === "mcp_tool") {
13693
+ const server = importCanonicalString({
13694
+ value: h.server,
13695
+ canonical: "server",
13696
+ warn
13697
+ });
13698
+ const tool = importCanonicalString({
13699
+ value: h.tool,
13700
+ canonical: "tool",
13701
+ warn
13702
+ });
13703
+ return {
13704
+ ...server !== void 0 && { server },
13705
+ ...tool !== void 0 && { tool },
13706
+ ...h.input !== null && typeof h.input === "object" && !Array.isArray(h.input) && { input: h.input }
13707
+ };
13708
+ }
13709
+ if (hookType === "prompt" || hookType === "agent") {
13710
+ const model = importCanonicalString({
13711
+ value: h.model,
13712
+ canonical: "model",
13713
+ warn
13714
+ });
13715
+ return model !== void 0 ? { model } : {};
13716
+ }
13538
13717
  return {};
13539
13718
  }
13540
13719
  /**
13541
- * Convert a single tool hook record into a canonical hook definition.
13720
+ * Import every per-hook passthrough kind for one tool hook record, reversing
13721
+ * the emit side in {@link buildToolHooks}.
13542
13722
  */
13543
- function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
13544
- const command = stripCommandPrefix({
13545
- command: h.command,
13546
- converterConfig
13547
- });
13548
- const hookType = isImportedHookType(h.type) ? h.type : "command";
13549
- const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
13550
- const prompt = typeof h.prompt === "string" ? h.prompt : void 0;
13723
+ function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
13551
13724
  return {
13552
- type: hookType,
13553
- ...command !== void 0 && command !== null && { command },
13554
- ...timeout !== void 0 && timeout !== null && { timeout },
13555
- ...prompt !== void 0 && prompt !== null && { prompt },
13556
- ...importTypePayloadFields({
13725
+ ...importPassthroughFields({
13557
13726
  h,
13558
- hookType
13727
+ hookType,
13728
+ fields: converterConfig.booleanPassthroughFields ?? [],
13729
+ isValid: isBooleanValue,
13730
+ warn
13559
13731
  }),
13560
- ...converterConfig.passthroughFields?.includes("name") && typeof h.name === "string" && { name: h.name },
13561
- ...converterConfig.passthroughFields?.includes("description") && typeof h.description === "string" && { description: h.description },
13562
- ...importBooleanPassthroughFields({
13732
+ ...importPassthroughFields({
13563
13733
  h,
13564
- converterConfig
13734
+ hookType,
13735
+ fields: converterConfig.numberPassthroughFields ?? [],
13736
+ isValid: isImportableNumber,
13737
+ describeInvalid: describeInvalidScalar,
13738
+ warn
13565
13739
  }),
13566
- ...importNumberPassthroughFields({
13740
+ ...importPassthroughFields({
13567
13741
  h,
13568
- converterConfig
13742
+ hookType,
13743
+ fields: converterConfig.stringPassthroughFields ?? [],
13744
+ isValid: isImportableString,
13745
+ describeInvalid: describeInvalidScalar,
13746
+ warn
13747
+ }),
13748
+ ...importPassthroughFields({
13749
+ h,
13750
+ hookType,
13751
+ fields: converterConfig.arrayPassthroughFields ?? [],
13752
+ isValid: isImportableArray,
13753
+ describeInvalid: describeInvalidArray,
13754
+ warn
13569
13755
  }),
13570
- ...importStringPassthroughFields({
13756
+ ...importPassthroughFields({
13571
13757
  h,
13758
+ hookType,
13759
+ fields: converterConfig.recordPassthroughFields ?? [],
13760
+ isValid: isEmittableRecord,
13761
+ describeInvalid: describeInvalidRecord,
13762
+ warn
13763
+ })
13764
+ };
13765
+ }
13766
+ /**
13767
+ * Convert a single tool hook record into a canonical hook definition.
13768
+ */
13769
+ function toolHookToCanonical({ h, rawEntry, converterConfig, warn }) {
13770
+ const hookType = isImportedHookType(h.type) ? h.type : "command";
13771
+ const command = importCanonicalString({
13772
+ value: stripCommandPrefix({
13773
+ command: h.command,
13572
13774
  converterConfig
13573
13775
  }),
13574
- ...importArrayPassthroughFields({
13776
+ canonical: "command",
13777
+ warn
13778
+ });
13779
+ const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
13780
+ const prompt = importCanonicalString({
13781
+ value: h.prompt,
13782
+ canonical: "prompt",
13783
+ warn
13784
+ });
13785
+ const name = importCanonicalString({
13786
+ value: h.name,
13787
+ canonical: "name",
13788
+ warn
13789
+ });
13790
+ const description = importCanonicalString({
13791
+ value: h.description,
13792
+ canonical: "description",
13793
+ warn
13794
+ });
13795
+ const matcher = rawEntry.matcher;
13796
+ return {
13797
+ type: hookType,
13798
+ ...command !== void 0 && command !== null && { command },
13799
+ ...timeout !== void 0 && timeout !== null && { timeout },
13800
+ ...prompt !== void 0 && prompt !== null && { prompt },
13801
+ ...importTypePayloadFields({
13575
13802
  h,
13576
- converterConfig,
13577
- logger
13803
+ hookType,
13804
+ warn
13578
13805
  }),
13579
- ...importRecordPassthroughFields({
13806
+ ...converterConfig.passthroughFields?.includes("name") && name !== void 0 && { name },
13807
+ ...converterConfig.passthroughFields?.includes("description") && description !== void 0 && { description },
13808
+ ...importAllPassthroughFields({
13580
13809
  h,
13581
13810
  hookType,
13582
13811
  converterConfig,
13583
- logger
13812
+ warn
13584
13813
  }),
13585
13814
  ...importGroupPassthroughFields({
13586
13815
  rawEntry,
13587
13816
  converterConfig
13588
13817
  }),
13589
- ...rawEntry.matcher !== void 0 && rawEntry.matcher !== null && rawEntry.matcher !== "" && { matcher: rawEntry.matcher }
13818
+ ...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
13590
13819
  };
13591
13820
  }
13592
13821
  /**
13593
- * Convert a single tool matcher entry into canonical hook definitions.
13822
+ * The fields whose value decides what a hook *is*, listed per hook type. When
13823
+ * one of them cannot be imported, dropping just the field would leave
13824
+ * something worse than nothing: a hook that loses its body runs nothing, and
13825
+ * one that loses its `matcher` fires on *everything* — a silent widening of
13826
+ * what the imported rule does. So the whole definition is skipped instead,
13827
+ * with the reason named. A field that does not define *this* type (a `prompt`
13828
+ * left on a command hook) is not one of them: it is dropped on its own, the
13829
+ * way any other unusable field is.
13594
13830
  */
13595
- function toolMatcherEntryToCanonical({ rawEntry, converterConfig, logger }) {
13596
- return (rawEntry.hooks ?? []).map((h) => toolHookToCanonical({
13831
+ function definingFields({ h, rawEntry, hookType, converterConfig }) {
13832
+ const fields = [{
13833
+ field: "matcher",
13834
+ value: rawEntry.matcher
13835
+ }];
13836
+ if (hookType === "command") fields.push({
13837
+ field: "command",
13838
+ value: typeof h.command === "string" ? stripCommandPrefix({
13839
+ command: h.command,
13840
+ converterConfig
13841
+ }) : h.command
13842
+ });
13843
+ if (hookType === "prompt" || hookType === "agent") fields.push({
13844
+ field: "prompt",
13845
+ value: h.prompt
13846
+ });
13847
+ if (hookType === "http") fields.push({
13848
+ field: "url",
13849
+ value: h.url
13850
+ });
13851
+ if (hookType === "mcp_tool") fields.push({
13852
+ field: "server",
13853
+ value: h.server
13854
+ }, {
13855
+ field: "tool",
13856
+ value: h.tool
13857
+ });
13858
+ return fields;
13859
+ }
13860
+ /**
13861
+ * Why no hook of this matcher group can be imported, or `undefined` when they
13862
+ * can. A group field that *restricts* when its hooks run (`subdividesGroup`)
13863
+ * is the group-level twin of `matcher`: importing the group without it would
13864
+ * widen every hook in it, so the group is skipped instead.
13865
+ */
13866
+ function describeGroupSkipReason({ rawEntry, converterConfig }) {
13867
+ const entry = rawEntry;
13868
+ for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
13869
+ const value = entry[tool];
13870
+ if (subdividesGroup !== true || value === void 0) continue;
13871
+ if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${JSON.stringify(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
13872
+ }
13873
+ }
13874
+ /**
13875
+ * Why this hook cannot be imported, or `undefined` when it can.
13876
+ */
13877
+ function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
13878
+ for (const { field, value } of definingFields({
13597
13879
  h,
13598
13880
  rawEntry,
13599
- converterConfig,
13600
- logger
13601
- }));
13881
+ hookType,
13882
+ converterConfig
13883
+ })) {
13884
+ if (value === void 0 || satisfiesCanonicalField({
13885
+ value,
13886
+ canonical: field
13887
+ })) continue;
13888
+ return `Skipping a hook while importing: its "${field}" (${JSON.stringify(value)}) is unusable — ${describeScalarConstraint({
13889
+ canonical: field,
13890
+ value
13891
+ })} Keeping the hook without it would change what it does, so the whole hook is skipped.`;
13892
+ }
13893
+ }
13894
+ /**
13895
+ * Convert a single tool matcher entry into canonical hook definitions.
13896
+ */
13897
+ function toolMatcherEntryToCanonical({ rawEntry, converterConfig, warn }) {
13898
+ const hookDefs = rawEntry.hooks ?? [];
13899
+ const groupSkipReason = describeGroupSkipReason({
13900
+ rawEntry,
13901
+ converterConfig
13902
+ });
13903
+ if (groupSkipReason !== void 0) {
13904
+ warn?.(groupSkipReason);
13905
+ return [];
13906
+ }
13907
+ const definitions = [];
13908
+ for (const h of hookDefs) {
13909
+ const skipReason = describeHookSkipReason({
13910
+ h,
13911
+ rawEntry,
13912
+ hookType: isImportedHookType(h.type) ? h.type : "command",
13913
+ converterConfig
13914
+ });
13915
+ if (skipReason !== void 0) {
13916
+ warn?.(skipReason);
13917
+ continue;
13918
+ }
13919
+ definitions.push(toolHookToCanonical({
13920
+ h,
13921
+ rawEntry,
13922
+ converterConfig,
13923
+ warn
13924
+ }));
13925
+ }
13926
+ return definitions;
13602
13927
  }
13603
13928
  /**
13604
13929
  * Assemble the canonical hooks config a tool importer writes to
@@ -13628,6 +13953,7 @@ function buildImportedHooksConfig({ hooks, overrideKey, version = 1, extraOverri
13628
13953
  }
13629
13954
  function toolHooksToCanonical({ hooks, converterConfig, logger }) {
13630
13955
  if (hooks === null || hooks === void 0 || typeof hooks !== "object") return {};
13956
+ const warn = warnOnce(logger);
13631
13957
  const canonical = {};
13632
13958
  for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
13633
13959
  const eventName = converterConfig.toolToCanonicalEventNames[toolEventName] ?? toolEventName;
@@ -13638,7 +13964,7 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
13638
13964
  defs.push(...toolMatcherEntryToCanonical({
13639
13965
  rawEntry,
13640
13966
  converterConfig,
13641
- logger
13967
+ warn
13642
13968
  }));
13643
13969
  }
13644
13970
  if (defs.length > 0) canonical[eventName] = defs;
@@ -13677,7 +14003,9 @@ const ANTIGRAVITY_HOOK_NAME = "rulesync";
13677
14003
  * map for import. Accepts both the documented named-hook shape
13678
14004
  * (`{ "<name>": { "<Event>": [...], "enabled"?: bool } }`) and a legacy flat
13679
14005
  * shape (`{ "<Event>": [...] }`) so older or hand-written files still import.
13680
- * The per-hook `enabled` flag is ignored (canonical hooks have no equivalent).
14006
+ * The per-hook `enabled` flag is ignored. The canonical `enabled` field is a
14007
+ * property of a single hook definition, whereas Antigravity's flag gates a whole
14008
+ * named group, so the two do not map onto each other.
13681
14009
  */
13682
14010
  function flattenAntigravityHooks(parsed) {
13683
14011
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -13759,7 +14087,7 @@ var AntigravityHooks = class extends ToolHooks {
13759
14087
  validate
13760
14088
  });
13761
14089
  }
13762
- toRulesyncHooks() {
14090
+ toRulesyncHooks({ logger } = {}) {
13763
14091
  let parsed;
13764
14092
  try {
13765
14093
  parsed = JSON.parse(this.getFileContent());
@@ -13768,7 +14096,8 @@ var AntigravityHooks = class extends ToolHooks {
13768
14096
  }
13769
14097
  const hooks = toolHooksToCanonical({
13770
14098
  hooks: flattenAntigravityHooks(parsed),
13771
- converterConfig: ANTIGRAVITY_CONVERTER_CONFIG
14099
+ converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
14100
+ logger
13772
14101
  });
13773
14102
  const overrideKey = this.constructor.getOverrideKey();
13774
14103
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
@@ -14150,7 +14479,7 @@ var ClaudecodeHooks = class extends ToolHooks {
14150
14479
  validate
14151
14480
  });
14152
14481
  }
14153
- toRulesyncHooks() {
14482
+ toRulesyncHooks({ logger } = {}) {
14154
14483
  let settings;
14155
14484
  try {
14156
14485
  settings = JSON.parse(this.getFileContent());
@@ -14159,7 +14488,8 @@ var ClaudecodeHooks = class extends ToolHooks {
14159
14488
  }
14160
14489
  const hooks = toolHooksToCanonical({
14161
14490
  hooks: settings.hooks,
14162
- converterConfig: this.constructor.getConverterConfig()
14491
+ converterConfig: this.constructor.getConverterConfig(),
14492
+ logger
14163
14493
  });
14164
14494
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
14165
14495
  hooks,
@@ -14318,13 +14648,14 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
14318
14648
  validate
14319
14649
  });
14320
14650
  }
14321
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
14651
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
14322
14652
  const paths = CodexcliHooks.getSettablePaths({ global });
14323
14653
  const config = rulesyncHooks.getJson();
14324
14654
  const codexHooks = canonicalToToolHooks({
14325
14655
  config,
14326
14656
  toolOverrideHooks: config.codexcli?.hooks,
14327
- converterConfig: CODEXCLI_CONVERTER_CONFIG
14657
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
14658
+ logger
14328
14659
  });
14329
14660
  const fileContent = JSON.stringify({ hooks: codexHooks }, null, 2);
14330
14661
  return new CodexcliHooks({
@@ -14335,7 +14666,7 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
14335
14666
  validate
14336
14667
  });
14337
14668
  }
14338
- toRulesyncHooks() {
14669
+ toRulesyncHooks({ logger } = {}) {
14339
14670
  let parsed;
14340
14671
  try {
14341
14672
  parsed = JSON.parse(this.getFileContent());
@@ -14344,7 +14675,8 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
14344
14675
  }
14345
14676
  const hooks = toolHooksToCanonical({
14346
14677
  hooks: parsed.hooks,
14347
- converterConfig: CODEXCLI_CONVERTER_CONFIG
14678
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
14679
+ logger
14348
14680
  });
14349
14681
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
14350
14682
  hooks,
@@ -14583,13 +14915,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
14583
14915
  * under `.github/hooks/` is picked up automatically when the CLI is
14584
14916
  * invoked from the project root.
14585
14917
  *
14586
- * - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` — chosen for
14587
- * consistency with the existing global Copilot CLI config layout (e.g.
14588
- * `~/.copilot/mcp-config.json` produced by `copilotcli-mcp.ts`). The
14589
- * official docs do not currently document a global hooks location, so
14590
- * this is a rulesync convention pending official documentation; we keep
14591
- * all rulesync-managed Copilot CLI files under the single `~/.copilot/`
14592
- * root and will revisit if the spec later mandates an alternate layout.
14918
+ * - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` — the directory is
14919
+ * the documented user-level hooks location ("`*.json` files in the
14920
+ * user-level hooks directory. By default this is `~/.copilot/hooks/` on
14921
+ * macOS and Linux, or `%USERPROFILE%\.copilot\hooks\` on Windows"). Every
14922
+ * `*.json` in it is loaded, so the filename remains rulesync's choice, as it
14923
+ * is for project scope. `COPILOT_HOME` relocates the directory upstream
14924
+ * (`$COPILOT_HOME/hooks/`); rulesync does not read that variable yet.
14593
14925
  *
14594
14926
  * Hook entries on the six matcher-aware events (see
14595
14927
  * {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
@@ -15000,17 +15332,18 @@ const DEEPAGENTS_MCP_FILE_NAME = ".mcp.json";
15000
15332
  const DEEPAGENTS_HOOKS_FILE_NAME = "hooks.json";
15001
15333
  //#endregion
15002
15334
  //#region src/features/hooks/deepagents-hooks.ts
15003
- function isDeepagentsHooksFile(val) {
15004
- if (typeof val !== "object" || val === null || !("hooks" in val)) return false;
15005
- return Array.isArray(val.hooks);
15335
+ function isRecord(value) {
15336
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15006
15337
  }
15007
15338
  /**
15008
- * Convert canonical hooks config to deepagents flat array format.
15339
+ * Convert the canonical hooks config to the deepagents Hooks v2 document.
15009
15340
  *
15010
- * deepagents format:
15011
- * { "hooks": [{ "command": ["bash", "-c", "..."], "events": ["session.start"] }] }
15341
+ * ```json
15342
+ * { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "" }] }] } }
15343
+ * ```
15012
15344
  *
15013
- * Each canonical hook definition becomes one deepagents hook entry.
15345
+ * Definitions sharing an event and matcher land in one group, preserving their
15346
+ * authored order — upstream runs a group's handlers in sequence.
15014
15347
  */
15015
15348
  function canonicalToDeepagentsHooks(config) {
15016
15349
  const supported = new Set(DEEPAGENTS_HOOK_EVENTS);
@@ -15018,7 +15351,7 @@ function canonicalToDeepagentsHooks(config) {
15018
15351
  ...config.hooks,
15019
15352
  ...config.deepagents?.hooks
15020
15353
  };
15021
- const entries = [];
15354
+ const hooks = {};
15022
15355
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
15023
15356
  if (!supported.has(canonicalEvent)) continue;
15024
15357
  const deepagentsEvent = CANONICAL_TO_DEEPAGENTS_EVENT_NAMES[canonicalEvent];
@@ -15026,43 +15359,69 @@ function canonicalToDeepagentsHooks(config) {
15026
15359
  for (const def of definitions) {
15027
15360
  if ((def.type ?? "command") !== "command") continue;
15028
15361
  if (!def.command) continue;
15029
- if (def.matcher) continue;
15030
- entries.push({
15031
- command: [
15032
- "bash",
15033
- "-c",
15034
- def.command
15035
- ],
15036
- events: [deepagentsEvent]
15362
+ const handler = {
15363
+ type: "command",
15364
+ command: def.command
15365
+ };
15366
+ if (def.timeout !== void 0 && def.timeout !== null && def.timeout > 0) handler.timeout = def.timeout;
15367
+ if (def.statusMessage !== void 0 && def.statusMessage !== null) handler.statusMessage = def.statusMessage;
15368
+ const matcher = def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" ? def.matcher : void 0;
15369
+ const groups = hooks[deepagentsEvent] ??= [];
15370
+ const group = groups.find((candidate) => candidate.matcher === matcher);
15371
+ if (group) group.hooks.push(handler);
15372
+ else groups.push({
15373
+ ...matcher !== void 0 && { matcher },
15374
+ hooks: [handler]
15037
15375
  });
15038
15376
  }
15039
15377
  }
15040
- return entries;
15378
+ return hooks;
15041
15379
  }
15042
15380
  /**
15043
- * Convert deepagents flat array format back to canonical hooks record.
15381
+ * Convert the Hooks v2 document back to the canonical hooks record.
15044
15382
  */
15045
- function deepagentsToCanonicalHooks(hooksEntries) {
15383
+ function deepagentsToCanonicalHooks(hooks) {
15046
15384
  const canonical = {};
15047
- for (const entry of hooksEntries) {
15048
- if (typeof entry !== "object" || entry === null) continue;
15049
- if (!Array.isArray(entry.command) || entry.command.length === 0) continue;
15050
- let command;
15051
- if (entry.command.length === 3 && entry.command[0] === "bash" && entry.command[1] === "-c") command = entry.command[2] ?? "";
15052
- else command = entry.command.join(" ");
15053
- const events = entry.events ?? [];
15054
- for (const deepagentsEvent of events) {
15055
- const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
15385
+ for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
15386
+ const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
15387
+ if (!canonicalEvent || !Array.isArray(groups)) continue;
15388
+ for (const group of groups) {
15389
+ if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
15390
+ for (const handler of group.hooks) {
15391
+ if (!isRecord(handler) || typeof handler.command !== "string") continue;
15392
+ const def = {
15393
+ type: "command",
15394
+ command: handler.command
15395
+ };
15396
+ if (typeof group.matcher === "string" && group.matcher !== "") def.matcher = group.matcher;
15397
+ if (typeof handler.timeout === "number") def.timeout = handler.timeout;
15398
+ if (typeof handler.statusMessage === "string") def.statusMessage = handler.statusMessage;
15399
+ (canonical[canonicalEvent] ??= []).push(def);
15400
+ }
15401
+ }
15402
+ }
15403
+ return canonical;
15404
+ }
15405
+ /**
15406
+ * Read the pre-v2 flat list. deepagents still loads it until 2026-09-01, so a
15407
+ * `hooks.json` a user has not migrated yet is imported rather than discarded —
15408
+ * but rulesync only ever writes the v2 shape, so regenerating migrates it.
15409
+ */
15410
+ function deepagentsLegacyToCanonicalHooks(entries) {
15411
+ const canonical = {};
15412
+ for (const entry of entries) {
15413
+ if (!isRecord(entry)) continue;
15414
+ const argv = entry.command;
15415
+ if (!Array.isArray(argv) || argv.length === 0) continue;
15416
+ const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
15417
+ const events = Array.isArray(entry.events) ? entry.events : [];
15418
+ for (const legacyEvent of events) {
15419
+ const canonicalEvent = typeof legacyEvent === "string" ? DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES[legacyEvent] : void 0;
15056
15420
  if (!canonicalEvent) continue;
15057
- const existing = canonical[canonicalEvent];
15058
- if (existing) existing.push({
15421
+ (canonical[canonicalEvent] ??= []).push({
15059
15422
  type: "command",
15060
15423
  command
15061
15424
  });
15062
- else canonical[canonicalEvent] = [{
15063
- type: "command",
15064
- command
15065
- }];
15066
15425
  }
15067
15426
  }
15068
15427
  return canonical;
@@ -15071,7 +15430,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15071
15430
  constructor(params) {
15072
15431
  super({
15073
15432
  ...params,
15074
- fileContent: params.fileContent ?? JSON.stringify({ hooks: [] }, null, 2)
15433
+ fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
15075
15434
  });
15076
15435
  }
15077
15436
  isDeletable() {
@@ -15085,7 +15444,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15085
15444
  }
15086
15445
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
15087
15446
  const paths = DeepagentsHooks.getSettablePaths({ global });
15088
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: [] }, null, 2);
15447
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} }, null, 2);
15089
15448
  return new DeepagentsHooks({
15090
15449
  outputRoot,
15091
15450
  relativeDirPath: paths.relativeDirPath,
@@ -15113,7 +15472,8 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15113
15472
  } catch (error) {
15114
15473
  throw new Error(`Failed to parse deepagents hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
15115
15474
  }
15116
- const hooks = deepagentsToCanonicalHooks(isDeepagentsHooksFile(parsed) ? parsed.hooks : []);
15475
+ const rawHooks = isRecord(parsed) ? parsed.hooks : void 0;
15476
+ const hooks = Array.isArray(rawHooks) ? deepagentsLegacyToCanonicalHooks(rawHooks) : isRecord(rawHooks) ? deepagentsToCanonicalHooks(rawHooks) : {};
15117
15477
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15118
15478
  hooks,
15119
15479
  overrideKey: "deepagents"
@@ -15130,7 +15490,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15130
15490
  outputRoot,
15131
15491
  relativeDirPath,
15132
15492
  relativeFilePath,
15133
- fileContent: JSON.stringify({ hooks: [] }, null, 2),
15493
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
15134
15494
  validate: false
15135
15495
  });
15136
15496
  }
@@ -15232,7 +15592,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
15232
15592
  validate
15233
15593
  });
15234
15594
  }
15235
- toRulesyncHooks() {
15595
+ toRulesyncHooks({ logger } = {}) {
15236
15596
  let parsed;
15237
15597
  try {
15238
15598
  parsed = JSON.parse(this.getFileContent());
@@ -15240,8 +15600,9 @@ var DevinHooks = class DevinHooks extends ToolHooks {
15240
15600
  throw new Error(`Failed to parse Devin hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
15241
15601
  }
15242
15602
  const hooks = toolHooksToCanonical({
15243
- hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
15244
- converterConfig: DEVIN_CONVERTER_CONFIG
15603
+ hooks: this.getRelativeFilePath() === "config.json" ? isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {} : parsed,
15604
+ converterConfig: DEVIN_CONVERTER_CONFIG,
15605
+ logger
15245
15606
  });
15246
15607
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15247
15608
  hooks,
@@ -15335,7 +15696,7 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
15335
15696
  validate
15336
15697
  });
15337
15698
  }
15338
- toRulesyncHooks() {
15699
+ toRulesyncHooks({ logger } = {}) {
15339
15700
  let settings;
15340
15701
  try {
15341
15702
  settings = JSON.parse(this.getFileContent());
@@ -15344,7 +15705,8 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
15344
15705
  }
15345
15706
  const hooks = toolHooksToCanonical({
15346
15707
  hooks: settings.hooks,
15347
- converterConfig: FACTORYDROID_CONVERTER_CONFIG
15708
+ converterConfig: FACTORYDROID_CONVERTER_CONFIG,
15709
+ logger
15348
15710
  });
15349
15711
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15350
15712
  hooks,
@@ -15413,13 +15775,14 @@ var GooseHooks = class GooseHooks extends ToolHooks {
15413
15775
  validate
15414
15776
  });
15415
15777
  }
15416
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
15778
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
15417
15779
  const paths = GooseHooks.getSettablePaths({ global });
15418
15780
  const config = rulesyncHooks.getJson();
15419
15781
  const gooseHooks = canonicalToToolHooks({
15420
15782
  config,
15421
15783
  toolOverrideHooks: config.goose?.hooks,
15422
- converterConfig: GOOSE_CONVERTER_CONFIG
15784
+ converterConfig: GOOSE_CONVERTER_CONFIG,
15785
+ logger
15423
15786
  });
15424
15787
  const fileContent = JSON.stringify({ hooks: gooseHooks }, null, 2);
15425
15788
  return new GooseHooks({
@@ -15430,7 +15793,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
15430
15793
  validate
15431
15794
  });
15432
15795
  }
15433
- toRulesyncHooks() {
15796
+ toRulesyncHooks({ logger } = {}) {
15434
15797
  let parsed;
15435
15798
  try {
15436
15799
  parsed = JSON.parse(this.getFileContent());
@@ -15439,7 +15802,8 @@ var GooseHooks = class GooseHooks extends ToolHooks {
15439
15802
  }
15440
15803
  const hooks = toolHooksToCanonical({
15441
15804
  hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
15442
- converterConfig: GOOSE_CONVERTER_CONFIG
15805
+ converterConfig: GOOSE_CONVERTER_CONFIG,
15806
+ logger
15443
15807
  });
15444
15808
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15445
15809
  hooks,
@@ -15543,7 +15907,7 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
15543
15907
  validate
15544
15908
  });
15545
15909
  }
15546
- toRulesyncHooks() {
15910
+ toRulesyncHooks({ logger } = {}) {
15547
15911
  let parsed;
15548
15912
  try {
15549
15913
  parsed = JSON.parse(this.getFileContent());
@@ -15551,8 +15915,9 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
15551
15915
  throw new Error(`Failed to parse Grok hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
15552
15916
  }
15553
15917
  const hooks = toolHooksToCanonical({
15554
- hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
15555
- converterConfig: GROKCLI_CONVERTER_CONFIG
15918
+ hooks: isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {},
15919
+ converterConfig: GROKCLI_CONVERTER_CONFIG,
15920
+ logger
15556
15921
  });
15557
15922
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15558
15923
  hooks,
@@ -15873,7 +16238,7 @@ var JunieHooks = class JunieHooks extends ToolHooks {
15873
16238
  validate
15874
16239
  });
15875
16240
  }
15876
- toRulesyncHooks() {
16241
+ toRulesyncHooks({ logger } = {}) {
15877
16242
  let settings;
15878
16243
  try {
15879
16244
  settings = JSON.parse(this.getFileContent());
@@ -15882,7 +16247,8 @@ var JunieHooks = class JunieHooks extends ToolHooks {
15882
16247
  }
15883
16248
  const hooks = toolHooksToCanonical({
15884
16249
  hooks: settings.hooks,
15885
- converterConfig: JUNIE_CONVERTER_CONFIG
16250
+ converterConfig: JUNIE_CONVERTER_CONFIG,
16251
+ logger
15886
16252
  });
15887
16253
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15888
16254
  hooks,
@@ -16620,7 +16986,7 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
16620
16986
  ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
16621
16987
  action,
16622
16988
  ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
16623
- enabled: true
16989
+ enabled: def.enabled ?? true
16624
16990
  });
16625
16991
  }
16626
16992
  return entries;
@@ -16660,6 +17026,7 @@ function kiroIdeHooksToCanonical(entries) {
16660
17026
  if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
16661
17027
  if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
16662
17028
  if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
17029
+ if (entry.enabled === false) def.enabled = false;
16663
17030
  (canonical[eventName] ??= []).push(def);
16664
17031
  }
16665
17032
  return canonical;
@@ -17604,6 +17971,37 @@ function unsupportedEventNames(params) {
17604
17971
  const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
17605
17972
  return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
17606
17973
  }
17974
+ /**
17975
+ * A logger whose warnings name the tool being converted, matching the rest of
17976
+ * this processor's warnings: the shared hooks converter says what is wrong but
17977
+ * not which tool's file it is reading or writing, and one run walks them all.
17978
+ *
17979
+ * Every method delegates explicitly rather than through a prototype, so the
17980
+ * real logger keeps owning its state — a wrapper that inherited it would
17981
+ * absorb the writes `configure` and `outputJson` make.
17982
+ */
17983
+ function withToolTargetPrefix({ logger, toolTarget }) {
17984
+ return {
17985
+ configure: (options) => logger.configure(options),
17986
+ get verbose() {
17987
+ return logger.verbose;
17988
+ },
17989
+ get silent() {
17990
+ return logger.silent;
17991
+ },
17992
+ get jsonMode() {
17993
+ return logger.jsonMode;
17994
+ },
17995
+ captureData: (key, value) => logger.captureData(key, value),
17996
+ getJsonData: () => logger.getJsonData(),
17997
+ outputJson: (success, error) => logger.outputJson(success, error),
17998
+ info: (message, ...args) => logger.info(message, ...args),
17999
+ success: (message, ...args) => logger.success(message, ...args),
18000
+ warn: (message, ...args) => logger.warn(`For ${toolTarget}: ${message}`, ...args),
18001
+ error: (message, code, ...args) => logger.error(message, code, ...args),
18002
+ debug: (message, ...args) => logger.debug(message, ...args)
18003
+ };
18004
+ }
17607
18005
  function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
17608
18006
  if (factory.supportsMatcher && !factory.matcherEvents) return [];
17609
18007
  const matcherEvents = factory.matcherEvents ? new Set(factory.matcherEvents) : void 0;
@@ -17826,13 +18224,13 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17826
18224
  ["deepagents", {
17827
18225
  class: DeepagentsHooks,
17828
18226
  meta: {
17829
- supportsProject: false,
18227
+ supportsProject: true,
17830
18228
  supportsGlobal: true,
17831
18229
  supportsImport: true
17832
18230
  },
17833
18231
  supportedEvents: DEEPAGENTS_HOOK_EVENTS,
17834
18232
  supportedHookTypes: ["command"],
17835
- supportsMatcher: false
18233
+ supportsMatcher: true
17836
18234
  }],
17837
18235
  ["kiro", {
17838
18236
  class: KiroHooks,
@@ -18044,6 +18442,15 @@ var HooksProcessor = class extends FeatureProcessor {
18044
18442
  }
18045
18443
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
18046
18444
  }
18445
+ if (this.toolTarget !== "kiro-ide") {
18446
+ const skippedEvents = new Set(unsupportedEventNames({
18447
+ factory,
18448
+ sharedHooks,
18449
+ effectiveHooks
18450
+ }));
18451
+ const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
18452
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only kiro-ide supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
18453
+ }
18047
18454
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
18048
18455
  factory,
18049
18456
  effectiveHooks
@@ -18054,7 +18461,10 @@ var HooksProcessor = class extends FeatureProcessor {
18054
18461
  rulesyncHooks,
18055
18462
  validate: true,
18056
18463
  global: this.global,
18057
- logger: this.logger
18464
+ logger: withToolTargetPrefix({
18465
+ logger: this.logger,
18466
+ toolTarget: this.toolTarget
18467
+ })
18058
18468
  })];
18059
18469
  const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
18060
18470
  outputRoot: this.outputRoot,
@@ -18064,7 +18474,12 @@ var HooksProcessor = class extends FeatureProcessor {
18064
18474
  return result;
18065
18475
  }
18066
18476
  async convertToolFilesToRulesyncFiles(toolFiles) {
18067
- return toolFiles.filter((f) => f instanceof ToolHooks).map((h) => h.toRulesyncHooks({ logger: this.logger }));
18477
+ const hooks = toolFiles.filter((f) => f instanceof ToolHooks);
18478
+ const logger = withToolTargetPrefix({
18479
+ logger: this.logger,
18480
+ toolTarget: this.toolTarget
18481
+ });
18482
+ return hooks.map((h) => h.toRulesyncHooks({ logger }));
18068
18483
  }
18069
18484
  static getToolTargets({ global = false, importOnly = false } = {}) {
18070
18485
  if (global) return importOnly ? hooksProcessorToolTargetsGlobalImportable : hooksProcessorToolTargetsGlobal;
@@ -19707,9 +20122,9 @@ function parseAmpSettingsJsonc(fileContent) {
19707
20122
  }
19708
20123
  function filterMcpServers(mcpServers) {
19709
20124
  const filtered = {};
19710
- if (!isRecord(mcpServers)) return filtered;
20125
+ if (!isRecord$1(mcpServers)) return filtered;
19711
20126
  for (const [name, config] of Object.entries(mcpServers)) {
19712
- if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
20127
+ if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
19713
20128
  const filteredConfig = {};
19714
20129
  for (const [key, value] of Object.entries(config)) {
19715
20130
  if (isPrototypePollutionKey(key)) continue;
@@ -19824,7 +20239,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
19824
20239
  success: true,
19825
20240
  error: null
19826
20241
  };
19827
- if (!isRecord(mcpServers)) return {
20242
+ if (!isRecord$1(mcpServers)) return {
19828
20243
  success: false,
19829
20244
  error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
19830
20245
  };
@@ -19833,7 +20248,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
19833
20248
  success: false,
19834
20249
  error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
19835
20250
  };
19836
- if (!isRecord(serverConfig)) return {
20251
+ if (!isRecord$1(serverConfig)) return {
19837
20252
  success: false,
19838
20253
  error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
19839
20254
  };
@@ -20433,13 +20848,13 @@ function normalizeCodexMcpServerName(name) {
20433
20848
  function convertFromCodexFormat(codexMcp) {
20434
20849
  const result = {};
20435
20850
  for (const [name, config] of Object.entries(codexMcp)) {
20436
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
20851
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
20437
20852
  const converted = {};
20438
20853
  for (const [key, value] of Object.entries(config)) {
20439
20854
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
20440
20855
  if (key === "enabled") {
20441
20856
  if (value === false) converted["disabled"] = true;
20442
- } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
20857
+ } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
20443
20858
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
20444
20859
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
20445
20860
  if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
@@ -20458,7 +20873,7 @@ function convertToCodexFormat(mcpServers) {
20458
20873
  const result = {};
20459
20874
  const originalNames = /* @__PURE__ */ new Map();
20460
20875
  for (const [name, config] of Object.entries(mcpServers)) {
20461
- if (!isRecord(config)) continue;
20876
+ if (!isRecord$1(config)) continue;
20462
20877
  const { codexName, usedFallback } = normalizeCodexMcpServerName(name);
20463
20878
  if (usedFallback) warnWithFallback(void 0, `MCP server "${name}" cannot be represented as a Codex MCP server name (ASCII [a-zA-Z0-9_-] only), so the stable fallback name "${codexName}" was used. Rename the server in .rulesync/mcp.jsonc to choose a readable Codex name.`);
20464
20879
  const converted = {};
@@ -20466,7 +20881,7 @@ function convertToCodexFormat(mcpServers) {
20466
20881
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
20467
20882
  if (key === "disabled") {
20468
20883
  if (value === true) converted["enabled"] = false;
20469
- } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
20884
+ } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
20470
20885
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
20471
20886
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
20472
20887
  if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
@@ -20542,21 +20957,21 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
20542
20957
  const strippedMcpServers = rulesyncMcp.getMcpServers();
20543
20958
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
20544
20959
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
20545
- const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
20960
+ const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
20546
20961
  return [serverName, {
20547
20962
  ...serverConfig,
20548
- ...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
20549
- ...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
20550
- ...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
20963
+ ...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
20964
+ ...isRecord$1(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
20965
+ ...isRecord$1(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
20551
20966
  }];
20552
20967
  })));
20553
20968
  const filteredMcpServers = this.removeEmptyEntries(converted);
20554
20969
  for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the codex CLI config`);
20555
- const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
20970
+ const existingMcpServers = isRecord$1(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
20556
20971
  const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
20557
- const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
20972
+ const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
20558
20973
  const serverRecord = serverConfig;
20559
- if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
20974
+ if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
20560
20975
  ...serverRecord,
20561
20976
  tools: existingServer["tools"]
20562
20977
  }];
@@ -21513,7 +21928,7 @@ function convertServerToGooseExtension(name, config, logger) {
21513
21928
  function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
21514
21929
  const generated = {};
21515
21930
  for (const [name, config] of Object.entries(mcpServers)) {
21516
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
21931
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
21517
21932
  const ext = convertServerToGooseExtension(name, config, logger);
21518
21933
  if (ext !== void 0) generated[name] = ext;
21519
21934
  }
@@ -21521,7 +21936,7 @@ function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
21521
21936
  const retracted = [];
21522
21937
  for (const [name, ext] of Object.entries(existingExtensions)) {
21523
21938
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
21524
- const type = isRecord(ext) ? existingExtensionType(ext) : void 0;
21939
+ const type = isRecord$1(ext) ? existingExtensionType(ext) : void 0;
21525
21940
  if (type !== void 0 && GOOSE_MCP_EXTENSION_TYPES.has(type)) {
21526
21941
  if (!Object.hasOwn(generated, name)) retracted.push(name);
21527
21942
  continue;
@@ -21555,7 +21970,7 @@ function convertFromGooseFormat(extensions) {
21555
21970
  const result = {};
21556
21971
  const skipped = [];
21557
21972
  for (const [name, ext] of Object.entries(extensions)) {
21558
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
21973
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
21559
21974
  const type = existingExtensionType(ext);
21560
21975
  if (type === void 0 || !GOOSE_MCP_EXTENSION_TYPES.has(type)) {
21561
21976
  skipped.push(name);
@@ -21609,7 +22024,7 @@ function buildGoosePluginStdioServer(config) {
21609
22024
  function convertToGoosePluginMcpServers(mcpServers, logger) {
21610
22025
  const result = {};
21611
22026
  for (const [name, config] of Object.entries(mcpServers)) {
21612
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22027
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
21613
22028
  const gooseType = resolveGooseType(config, resolveGooseUrl(config));
21614
22029
  if (gooseType !== "stdio") {
21615
22030
  warnWithFallback(logger, `Goose open-plugin MCP manifest (${GOOSE_PLUGIN_MCP_RELATIVE_PATH}) is stdio-only; skipping "${name}" (${gooseType}). Sync it with --global to ~/.config/goose/config.yaml instead.`);
@@ -21650,7 +22065,7 @@ var GooseMcp = class GooseMcp extends ToolMcp {
21650
22065
  parsePluginManifest(fileContent) {
21651
22066
  try {
21652
22067
  const parsed = JSON.parse(fileContent);
21653
- return isRecord(parsed) ? parsed : {};
22068
+ return isRecord$1(parsed) ? parsed : {};
21654
22069
  } catch (error) {
21655
22070
  throw new Error(`Failed to parse Goose MCP manifest at ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
21656
22071
  }
@@ -21699,7 +22114,7 @@ var GooseMcp = class GooseMcp extends ToolMcp {
21699
22114
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21700
22115
  const existingContent = await readFileContentOrNull(filePath) ?? "";
21701
22116
  const config = parseGooseConfig(existingContent, paths.relativeDirPath, paths.relativeFilePath);
21702
- const existingExtensions = isRecord(config.extensions) ? config.extensions : {};
22117
+ const existingExtensions = isRecord$1(config.extensions) ? config.extensions : {};
21703
22118
  return new GooseMcp({
21704
22119
  outputRoot,
21705
22120
  relativeDirPath: paths.relativeDirPath,
@@ -21721,10 +22136,10 @@ var GooseMcp = class GooseMcp extends ToolMcp {
21721
22136
  }
21722
22137
  toRulesyncMcp() {
21723
22138
  if (!this.global) {
21724
- const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
22139
+ const mcpServers = isRecord$1(this.config.mcpServers) ? this.config.mcpServers : {};
21725
22140
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
21726
22141
  }
21727
- const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
22142
+ const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
21728
22143
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
21729
22144
  }
21730
22145
  validate() {
@@ -21760,7 +22175,7 @@ function convertToGrokFormat(mcpServers) {
21760
22175
  const result = {};
21761
22176
  for (const [name, config] of Object.entries(mcpServers)) {
21762
22177
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
21763
- if (!isRecord(config)) continue;
22178
+ if (!isRecord$1(config)) continue;
21764
22179
  const converted = {};
21765
22180
  for (const [key, value] of Object.entries(config)) {
21766
22181
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -21775,7 +22190,7 @@ function convertToGrokFormat(mcpServers) {
21775
22190
  function convertFromGrokFormat(grokMcp) {
21776
22191
  const result = {};
21777
22192
  for (const [name, config] of Object.entries(grokMcp)) {
21778
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22193
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
21779
22194
  const converted = {};
21780
22195
  for (const [key, value] of Object.entries(config)) {
21781
22196
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -21913,7 +22328,7 @@ function resolveHermesTimeout(config) {
21913
22328
  * import alike. See the Hermes mcp-config-reference.
21914
22329
  */
21915
22330
  function copyHermesOauth(source) {
21916
- if (!isRecord(source)) return;
22331
+ if (!isRecord$1(source)) return;
21917
22332
  const oauth = {};
21918
22333
  for (const key of [
21919
22334
  "redirect_uri",
@@ -22047,13 +22462,13 @@ function convertServerToHermes(config) {
22047
22462
  function convertToHermesFormat(mcpServers) {
22048
22463
  const result = {};
22049
22464
  for (const [name, config] of Object.entries(mcpServers)) {
22050
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22465
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
22051
22466
  result[name] = convertServerToHermes(config);
22052
22467
  }
22053
22468
  return result;
22054
22469
  }
22055
22470
  function mergeHermesMcpServers(config, mcpServers) {
22056
- const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
22471
+ const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
22057
22472
  return {
22058
22473
  ...config,
22059
22474
  mcp_servers: {
@@ -22072,7 +22487,7 @@ function convertFromHermesFormat(mcpServers) {
22072
22487
  const result = {};
22073
22488
  const hermesOverrides = {};
22074
22489
  for (const [name, config] of Object.entries(mcpServers)) {
22075
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22490
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
22076
22491
  const server = {};
22077
22492
  if (typeof config.command === "string") server.command = config.command;
22078
22493
  if (isStringArray$1(config.args)) server.args = config.args;
@@ -22081,7 +22496,7 @@ function convertFromHermesFormat(mcpServers) {
22081
22496
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
22082
22497
  if (config.enabled === false) server.disabled = true;
22083
22498
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
22084
- if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
22499
+ if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
22085
22500
  result[name] = server;
22086
22501
  const hermesServer = { ...server };
22087
22502
  if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
@@ -22120,7 +22535,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
22120
22535
  const merged = mergeHermesMcpServers(parseSharedConfig({
22121
22536
  format: "yaml",
22122
22537
  fileContent
22123
- }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
22538
+ }), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
22124
22539
  this.config = merged;
22125
22540
  super.setFileContent(applySharedConfigPatch({
22126
22541
  fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
@@ -22184,7 +22599,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
22184
22599
  });
22185
22600
  }
22186
22601
  toRulesyncMcp() {
22187
- const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
22602
+ const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
22188
22603
  return this.toRulesyncMcpDefault({
22189
22604
  outputRoot: getHermesagentRulesyncOutputRoot({
22190
22605
  nativeOutputRoot: this.outputRoot,
@@ -22496,7 +22911,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
22496
22911
  */
22497
22912
  function readExistingKiloMcpEntries(fileContent) {
22498
22913
  const mcp = (0, jsonc_parser.parse)(fileContent || "{}")?.mcp;
22499
- if (!isRecord(mcp)) return {};
22914
+ if (!isRecord$1(mcp)) return {};
22500
22915
  const entries = {};
22501
22916
  for (const [serverName, entry] of Object.entries(mcp)) {
22502
22917
  const result = KiloMcpServerSchema.safeParse(entry);
@@ -22800,7 +23215,7 @@ async function readKimiCodeConfig({ outputRoot }) {
22800
23215
  return {
22801
23216
  parsed: true,
22802
23217
  content,
22803
- mcp: isRecord(mcp) ? mcp : {}
23218
+ mcp: isRecord$1(mcp) ? mcp : {}
22804
23219
  };
22805
23220
  } catch {
22806
23221
  return {
@@ -22954,7 +23369,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
22954
23369
  static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
22955
23370
  if (!global) return [];
22956
23371
  const block = rulesyncMcp.getJson()["kimi-code"];
22957
- if (!isRecord(block)) return [];
23372
+ if (!isRecord$1(block)) return [];
22958
23373
  const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
22959
23374
  const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
22960
23375
  if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0) return [];
@@ -23007,6 +23422,68 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
23007
23422
  };
23008
23423
  //#endregion
23009
23424
  //#region src/features/mcp/kiro-mcp.ts
23425
+ /**
23426
+ * Union of two optional string lists, preserving order and dropping duplicates.
23427
+ * Returns `undefined` only when neither side was authored at all, so the caller
23428
+ * omits the key entirely rather than writing an empty array — but an explicitly
23429
+ * authored `[]` is kept, which keeps import → generate idempotent.
23430
+ */
23431
+ function mergeToolLists(...lists) {
23432
+ if (lists.every((list) => list === void 0)) return void 0;
23433
+ const merged = [];
23434
+ for (const list of lists) for (const tool of list ?? []) if (!merged.includes(tool)) merged.push(tool);
23435
+ return merged;
23436
+ }
23437
+ /**
23438
+ * Translate rulesync's Kiro-only authoring keys onto the field names Kiro
23439
+ * actually reads in `mcp.json`.
23440
+ *
23441
+ * - `kiroAutoApprove` → `autoApprove` (tools run without a confirmation prompt)
23442
+ * - `kiroAutoBlock` → `disabledTools` (tools hidden from the agent)
23443
+ *
23444
+ * `disabledTools` is the only block list Kiro reads, and it is also a canonical
23445
+ * rulesync field, so `kiroAutoBlock` is a redundant spelling of it. Prefer the
23446
+ * canonical field; see the note on `kiroAutoBlock` in `src/types/mcp.ts`.
23447
+ *
23448
+ * Both native names are documented per-server fields, so a config that already
23449
+ * spells them natively keeps working: the two lists are merged rather than
23450
+ * one overwriting the other.
23451
+ * @see https://kiro.dev/docs/mcp/configuration/
23452
+ */
23453
+ function toKiroMcpServers(servers) {
23454
+ return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
23455
+ const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
23456
+ const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
23457
+ const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
23458
+ return [name, {
23459
+ ...rest,
23460
+ ...autoApprove !== void 0 && { autoApprove },
23461
+ ...disabled !== void 0 && { disabledTools: disabled }
23462
+ }];
23463
+ }));
23464
+ }
23465
+ /**
23466
+ * Import direction of {@link toKiroMcpServers}: Kiro's `autoApprove` becomes the
23467
+ * rulesync-only `kiroAutoApprove` so a regenerate reproduces it. `disabledTools`
23468
+ * is left alone — it is already a canonical rulesync key with the same meaning,
23469
+ * so `kiroAutoBlock` deliberately has no import counterpart.
23470
+ *
23471
+ * Only a genuine string array is renamed. `kiroAutoApprove` is typed as one, so
23472
+ * moving a hand-written `"autoApprove": "all"` there would produce a
23473
+ * `.rulesync/mcp.jsonc` the next generate refuses to parse; such a value stays
23474
+ * under its original key and passes through untouched instead.
23475
+ */
23476
+ function fromKiroMcpServers(servers) {
23477
+ return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
23478
+ if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
23479
+ const { autoApprove, ...rest } = server;
23480
+ if (!isStringArray$1(autoApprove)) return [name, server];
23481
+ return [name, {
23482
+ ...rest,
23483
+ kiroAutoApprove: autoApprove
23484
+ }];
23485
+ }));
23486
+ }
23010
23487
  var KiroMcp = class KiroMcp extends ToolMcp {
23011
23488
  json;
23012
23489
  constructor(params) {
@@ -23035,7 +23512,7 @@ var KiroMcp = class KiroMcp extends ToolMcp {
23035
23512
  }
23036
23513
  static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
23037
23514
  const paths = this.getSettablePaths();
23038
- const fileContent = JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2);
23515
+ const fileContent = JSON.stringify({ mcpServers: toKiroMcpServers(rulesyncMcp.getMcpServers()) }, null, 2);
23039
23516
  return new KiroMcp({
23040
23517
  outputRoot,
23041
23518
  relativeDirPath: paths.relativeDirPath,
@@ -23045,7 +23522,9 @@ var KiroMcp = class KiroMcp extends ToolMcp {
23045
23522
  });
23046
23523
  }
23047
23524
  toRulesyncMcp() {
23048
- return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: this.json.mcpServers ?? {} }, null, 2) });
23525
+ const mcpServers = this.json.mcpServers;
23526
+ const translated = isMcpServers(mcpServers) ? fromKiroMcpServers(mcpServers) : {};
23527
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: translated }, null, 2) });
23049
23528
  }
23050
23529
  validate() {
23051
23530
  return {
@@ -23931,7 +24410,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
23931
24410
  });
23932
24411
  }
23933
24412
  function disabledNamesOf(config) {
23934
- const mcpBlock = config && isRecord(config.mcp) ? config.mcp : {};
24413
+ const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
23935
24414
  return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
23936
24415
  }
23937
24416
  /**
@@ -24050,7 +24529,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
24050
24529
  const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
24051
24530
  const disabledNames = managedNames.filter((name) => {
24052
24531
  const server = servers[name];
24053
- return isRecord(server) && server.disabled === true;
24532
+ return isRecord$1(server) && server.disabled === true;
24054
24533
  });
24055
24534
  const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)) ?? "";
24056
24535
  let existingParsed;
@@ -24064,7 +24543,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
24064
24543
  logger?.warn(`Skipping the Rovo Dev mcp.disabledMcpServers update: ${formatError(error)}`);
24065
24544
  return [];
24066
24545
  }
24067
- const existingMcp = isRecord(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
24546
+ const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
24068
24547
  const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
24069
24548
  const managedNameSet = new Set(managedNames);
24070
24549
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
@@ -24256,7 +24735,7 @@ function deriveTransportAllowlist(servers) {
24256
24735
  http: false
24257
24736
  };
24258
24737
  for (const server of Object.values(servers)) {
24259
- if (!isRecord(server)) continue;
24738
+ if (!isRecord$1(server)) continue;
24260
24739
  const transport = transportOf(server);
24261
24740
  if (transport) allowlist[transport] = true;
24262
24741
  }
@@ -27891,13 +28370,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
27891
28370
  WebFetch: "webfetch",
27892
28371
  Mcp: "mcp"
27893
28372
  };
27894
- const MCP_CANONICAL_PREFIX$1 = "mcp__";
28373
+ const MCP_CANONICAL_PREFIX$2 = "mcp__";
27895
28374
  /**
27896
28375
  * Returns true if the canonical category is the per-tool MCP form
27897
28376
  * `mcp__<server>__<tool>`.
27898
28377
  */
27899
28378
  function isMcpScopedCategory(canonical) {
27900
- return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
28379
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$2) && canonical.length > 5;
27901
28380
  }
27902
28381
  function toCursorType(canonical) {
27903
28382
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -27931,7 +28410,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
27931
28410
  if (match) {
27932
28411
  const server = match[1] ?? "*";
27933
28412
  const tool = match[2] ?? "*";
27934
- return `${MCP_CANONICAL_PREFIX$1}${server}__${tool}`;
28413
+ return `${MCP_CANONICAL_PREFIX$2}${server}__${tool}`;
27935
28414
  }
27936
28415
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
27937
28416
  }
@@ -28160,7 +28639,7 @@ function convertCursorToRulesyncPermissions(params) {
28160
28639
  const { type, pattern } = parseCursorPermissionEntry(entry);
28161
28640
  const canonical = toCanonicalCategory$1(type, pattern);
28162
28641
  if (!permission[canonical]) permission[canonical] = {};
28163
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
28642
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$2) ? "*" : pattern;
28164
28643
  permission[canonical][canonicalPattern] = action;
28165
28644
  }
28166
28645
  };
@@ -28295,14 +28774,14 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
28295
28774
  let settings;
28296
28775
  try {
28297
28776
  const parsed = JSON.parse(existingContent);
28298
- settings = isRecord(parsed) ? parsed : {};
28777
+ settings = isRecord$1(parsed) ? parsed : {};
28299
28778
  } catch (error) {
28300
28779
  throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
28301
28780
  }
28302
28781
  const config = rulesyncPermissions.getJson();
28303
28782
  const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
28304
28783
  const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
28305
- const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
28784
+ const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
28306
28785
  const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
28307
28786
  const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
28308
28787
  const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
@@ -28332,11 +28811,11 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
28332
28811
  let settings;
28333
28812
  try {
28334
28813
  const parsed = JSON.parse(this.getFileContent());
28335
- settings = isRecord(parsed) ? parsed : {};
28814
+ settings = isRecord$1(parsed) ? parsed : {};
28336
28815
  } catch (error) {
28337
28816
  throw new Error(`Failed to parse Devin permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28338
28817
  }
28339
- const permissions = isRecord(settings.permissions) ? settings.permissions : {};
28818
+ const permissions = isRecord$1(settings.permissions) ? settings.permissions : {};
28340
28819
  const config = convertDevinToRulesyncPermissions({
28341
28820
  allow: Array.isArray(permissions.allow) ? permissions.allow : [],
28342
28821
  ask: Array.isArray(permissions.ask) ? permissions.ask : [],
@@ -28685,7 +29164,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28685
29164
  } catch (error) {
28686
29165
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
28687
29166
  }
28688
- const config = isRecord(parsed) ? { ...parsed } : {};
29167
+ const config = isRecord$1(parsed) ? { ...parsed } : {};
28689
29168
  const userPermission = convertRulesyncToGoosePermissionConfig({
28690
29169
  config: rulesyncPermissions.getJson(),
28691
29170
  logger
@@ -28708,8 +29187,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28708
29187
  } catch (error) {
28709
29188
  throw new Error(`Failed to parse Goose permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28710
29189
  }
28711
- const config = isRecord(parsed) ? parsed : {};
28712
- const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
29190
+ const config = isRecord$1(parsed) ? parsed : {};
29191
+ const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
28713
29192
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
28714
29193
  }
28715
29194
  validate() {
@@ -28783,7 +29262,7 @@ const GROKCLI_UI_KEY = "ui";
28783
29262
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
28784
29263
  const GROKCLI_PERMISSION_KEY = "permission";
28785
29264
  const CATCH_ALL_PATTERN$2 = "*";
28786
- const MCP_CANONICAL_PREFIX = "mcp__";
29265
+ const MCP_CANONICAL_PREFIX$1 = "mcp__";
28787
29266
  const CATEGORY_TO_GROK_TOOL = {
28788
29267
  bash: "Bash",
28789
29268
  read: "Read",
@@ -28811,7 +29290,7 @@ const GROK_MCP_TOOL = "MCPTool";
28811
29290
  * concrete pattern emits `Tool(pattern)`.
28812
29291
  */
28813
29292
  function buildGrokEntry(category, pattern) {
28814
- if (category.startsWith(MCP_CANONICAL_PREFIX)) {
29293
+ if (category.startsWith(MCP_CANONICAL_PREFIX$1)) {
28815
29294
  const remainder = category.slice(5);
28816
29295
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
28817
29296
  }
@@ -28838,7 +29317,7 @@ function parseGrokEntry(entry) {
28838
29317
  inner = trimmed.slice(parenIndex + 1, -1).trim();
28839
29318
  }
28840
29319
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
28841
- category: `${MCP_CANONICAL_PREFIX}${inner}`,
29320
+ category: `${MCP_CANONICAL_PREFIX$1}${inner}`,
28842
29321
  pattern: CATCH_ALL_PATTERN$2
28843
29322
  } : {
28844
29323
  category: "mcp",
@@ -28936,7 +29415,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
28936
29415
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
28937
29416
  }
28938
29417
  const config = rulesyncPermissions.getJson();
28939
- const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
29418
+ const existingPermission = isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
28940
29419
  const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
28941
29420
  const permission = {
28942
29421
  ...existingPermission,
@@ -28945,7 +29424,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
28945
29424
  ask: buckets.ask
28946
29425
  };
28947
29426
  const uiPatch = global ? { [GROKCLI_UI_KEY]: {
28948
- ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
29427
+ ...isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
28949
29428
  [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
28950
29429
  } } : {};
28951
29430
  return new GrokcliPermissions({
@@ -28974,7 +29453,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
28974
29453
  } catch (error) {
28975
29454
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28976
29455
  }
28977
- const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
29456
+ const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
28978
29457
  const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
28979
29458
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
28980
29459
  }
@@ -29071,7 +29550,7 @@ function parseGrokPermissionArrays(permission) {
29071
29550
  * `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
29072
29551
  */
29073
29552
  function legacyModeAction(parsed) {
29074
- return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
29553
+ return (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
29075
29554
  }
29076
29555
  /**
29077
29556
  * Collapse a rulesync permissions config into Grok's single coarse mode.
@@ -29127,12 +29606,12 @@ function withoutKey(record, key) {
29127
29606
  return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
29128
29607
  }
29129
29608
  function buildHermesOverride(config, provenance) {
29130
- const base = isRecord(provenance.hermes) ? { ...provenance.hermes } : {};
29131
- const approvalsOverride = withoutKey(isRecord(config.approvals) ? config.approvals : {}, "deny");
29609
+ const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
29610
+ const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
29132
29611
  if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
29133
29612
  else delete base.approvals;
29134
- const security = isRecord(config.security) ? { ...config.security } : {};
29135
- const blocklist = isRecord(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
29613
+ const security = isRecord$1(config.security) ? { ...config.security } : {};
29614
+ const blocklist = isRecord$1(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
29136
29615
  if (blocklist?.enabled === true) {
29137
29616
  delete blocklist.domains;
29138
29617
  delete blocklist.enabled;
@@ -29141,7 +29620,7 @@ function buildHermesOverride(config, provenance) {
29141
29620
  else delete security.website_blocklist;
29142
29621
  if (Object.keys(security).length > 0) base.security = security;
29143
29622
  else delete base.security;
29144
- for (const key of ["skills", "memory"]) if (isRecord(config[key])) base[key] = config[key];
29623
+ for (const key of ["skills", "memory"]) if (isRecord$1(config[key])) base[key] = config[key];
29145
29624
  else delete base[key];
29146
29625
  return base;
29147
29626
  }
@@ -29213,7 +29692,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
29213
29692
  format: "yaml",
29214
29693
  fileContent: this.getFileContent()
29215
29694
  });
29216
- const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
29695
+ const permissionsRoot = isRecord$1(config.permissions) ? config.permissions : {};
29217
29696
  const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(permissionsRoot.rulesync);
29218
29697
  const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
29219
29698
  const permission = clonePermissionBlock(provenance.permission);
@@ -29221,14 +29700,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
29221
29700
  permission,
29222
29701
  commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
29223
29702
  });
29224
- const approvals = isRecord(config.approvals) ? config.approvals : {};
29703
+ const approvals = isRecord$1(config.approvals) ? config.approvals : {};
29225
29704
  reconcileNativeDenies({
29226
29705
  permission,
29227
29706
  category: "bash",
29228
29707
  patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
29229
29708
  });
29230
- const security = isRecord(config.security) ? config.security : {};
29231
- const websiteBlocklist = isRecord(security.website_blocklist) ? security.website_blocklist : {};
29709
+ const security = isRecord$1(config.security) ? config.security : {};
29710
+ const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
29232
29711
  reconcileNativeDenies({
29233
29712
  permission,
29234
29713
  category: "webfetch",
@@ -29903,8 +30382,8 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
29903
30382
  existing = void 0;
29904
30383
  }
29905
30384
  const merged = {
29906
- ...isRecord(existing) ? existing : {},
29907
- ...isRecord(patch.tools) ? patch.tools : {}
30385
+ ...isRecord$1(existing) ? existing : {},
30386
+ ...isRecord$1(patch.tools) ? patch.tools : {}
29908
30387
  };
29909
30388
  if (Object.keys(merged).length === 0) return;
29910
30389
  warnAboutMistypedToolLists(merged);
@@ -29928,7 +30407,7 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
29928
30407
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#tools
29929
30408
  */
29930
30409
  function buildKimiCodeToolsSection(tools) {
29931
- if (!isRecord(tools)) return;
30410
+ if (!isRecord$1(tools)) return;
29932
30411
  const section = Object.fromEntries(Object.entries(tools).filter(([key, value]) => key === "enabled" || key === "disabled" ? isStringList(value) : true));
29933
30412
  return Object.keys(section).length > 0 ? section : void 0;
29934
30413
  }
@@ -30027,7 +30506,7 @@ function preserveKimiCodeRules(rules) {
30027
30506
  nativeRules
30028
30507
  };
30029
30508
  for (const raw of rules) {
30030
- if (!isRecord(raw)) continue;
30509
+ if (!isRecord$1(raw)) continue;
30031
30510
  const decision = raw.decision;
30032
30511
  const pattern = raw.pattern;
30033
30512
  if (decision !== "allow" && decision !== "ask" && decision !== "deny" || typeof pattern !== "string") continue;
@@ -30142,7 +30621,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
30142
30621
  format: "toml",
30143
30622
  fileContent: this.getFileContent()
30144
30623
  });
30145
- const { permission, nativeRules } = preserveKimiCodeRules((isRecord(config.permission) ? config.permission : {}).rules);
30624
+ const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
30146
30625
  const defaultPermissionMode = config.default_permission_mode;
30147
30626
  const tools = buildKimiCodeToolsSection(config.tools);
30148
30627
  const toolOverride = {
@@ -31419,7 +31898,7 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
31419
31898
  } catch (error) {
31420
31899
  throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
31421
31900
  }
31422
- const config = isRecord(parsed) ? { ...parsed } : {};
31901
+ const config = isRecord$1(parsed) ? { ...parsed } : {};
31423
31902
  const rulesyncConfig = rulesyncPermissions.getJson();
31424
31903
  const toolPermissions = convertRulesyncToRovodevToolPermissions({
31425
31904
  config: rulesyncConfig,
@@ -31456,8 +31935,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
31456
31935
  } catch (error) {
31457
31936
  throw new Error(`Failed to parse Rovodev permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
31458
31937
  }
31459
- const config = isRecord(parsed) ? parsed : {};
31460
- const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
31938
+ const config = isRecord$1(parsed) ? parsed : {};
31939
+ const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
31461
31940
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
31462
31941
  }
31463
31942
  validate() {
@@ -31484,14 +31963,14 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
31484
31963
  * keys it does not are kept as-is.
31485
31964
  */
31486
31965
  function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
31487
- const existingToolPermissions = isRecord(existing) ? { ...existing } : {};
31966
+ const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
31488
31967
  if (Object.keys(generated).length === 0 && sourceStatesRules) {
31489
- if (!isRecord(existing)) return;
31968
+ if (!isRecord$1(existing)) return;
31490
31969
  const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
31491
31970
  logger?.warn(`Rovo Dev permissions: the rulesync source produced no rule Rovo Dev can express, so the toolPermissions block in ${filePath} keeps its current levels` + (strippedKeys.length > 0 ? `, minus the grants ${strippedKeys.map((key) => `"${key}"`).join(", ")}.` : `.`));
31492
31971
  return existingToolPermissions;
31493
31972
  }
31494
- const hasExistingToolsRecord = isRecord(existingToolPermissions.tools);
31973
+ const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
31495
31974
  const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
31496
31975
  warnAboutDroppedOwnedKeys({
31497
31976
  existingToolPermissions,
@@ -31535,7 +32014,7 @@ function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, gen
31535
32014
  */
31536
32015
  function stripPermissiveOwnedValues(toolPermissions) {
31537
32016
  const strippedKeys = [];
31538
- if (isRecord(toolPermissions.tools)) {
32017
+ if (isRecord$1(toolPermissions.tools)) {
31539
32018
  const tools = { ...toolPermissions.tools };
31540
32019
  for (const toolKey of MANAGED_TOOL_KEYS) if (tools[toolKey] === "allow") {
31541
32020
  delete tools[toolKey];
@@ -31557,14 +32036,14 @@ function stripPermissiveOwnedValues(toolPermissions) {
31557
32036
  strippedKeys.push("default");
31558
32037
  }
31559
32038
  const bash = toolPermissions.bash;
31560
- if (isRecord(bash)) {
32039
+ if (isRecord$1(bash)) {
31561
32040
  const stripped = { ...bash };
31562
32041
  if (stripped.default === "allow") {
31563
32042
  delete stripped.default;
31564
32043
  strippedKeys.push("bash.default");
31565
32044
  }
31566
32045
  if (Array.isArray(stripped.commands)) {
31567
- const kept = stripped.commands.filter((entry) => !(isRecord(entry) && entry.permission === "allow"));
32046
+ const kept = stripped.commands.filter((entry) => !(isRecord$1(entry) && entry.permission === "allow"));
31568
32047
  if (kept.length !== stripped.commands.length) strippedKeys.push("bash.commands");
31569
32048
  if (kept.length > 0) stripped.commands = kept;
31570
32049
  else delete stripped.commands;
@@ -31672,15 +32151,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
31672
32151
  const permission = {};
31673
32152
  if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
31674
32153
  const bash = toolPermissions.bash;
31675
- if (isRecord(bash)) {
32154
+ if (isRecord$1(bash)) {
31676
32155
  const bashRules = {};
31677
32156
  if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
31678
32157
  if (Array.isArray(bash.commands)) {
31679
- for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
32158
+ for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
31680
32159
  }
31681
32160
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
31682
32161
  }
31683
- const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
32162
+ const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
31684
32163
  const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
31685
32164
  for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
31686
32165
  const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
@@ -32395,11 +32874,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
32395
32874
  config,
32396
32875
  logger
32397
32876
  });
32398
- const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
32399
- const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
32877
+ const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
32878
+ const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
32400
32879
  const override = config.warp;
32401
- const executionProfileOverride = isRecord(override) && isRecord(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
32402
- if (isRecord(override)) {
32880
+ const executionProfileOverride = isRecord$1(override) && isRecord$1(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
32881
+ if (isRecord$1(override)) {
32403
32882
  const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
32404
32883
  Object.assign(profiles, legacyOverride);
32405
32884
  }
@@ -32434,10 +32913,10 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
32434
32913
  } catch (error) {
32435
32914
  throw new Error(`Failed to parse Warp permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
32436
32915
  }
32437
- const agents = isRecord(settings.agents) ? settings.agents : {};
32438
- const profiles = isRecord(agents.profiles) ? agents.profiles : {};
32439
- const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
32440
- const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
32916
+ const agents = isRecord$1(settings.agents) ? settings.agents : {};
32917
+ const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
32918
+ const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
32919
+ const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
32441
32920
  const config = convertWarpToRulesyncPermissions({
32442
32921
  allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
32443
32922
  deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
@@ -32484,12 +32963,12 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
32484
32963
  */
32485
32964
  function mergeIntoDefaultExecutionProfile({ agents, mergedAllow, mergedDeny, executionProfileOverride, logger }) {
32486
32965
  const hasOverrideKeys = executionProfileOverride !== void 0 && Object.keys(executionProfileOverride).length > 0;
32487
- if (!isRecord(agents[EXECUTION_PROFILES_KEY])) {
32966
+ if (!isRecord$1(agents[EXECUTION_PROFILES_KEY])) {
32488
32967
  if (hasOverrideKeys && logger) logger.warn("The warp.execution_profile permissions override was skipped: settings.toml has no [agents.execution_profiles] collection yet (un-migrated install). Open Warp once to run its settings migration, then re-run rulesync generate.");
32489
32968
  return;
32490
32969
  }
32491
32970
  const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
32492
- const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
32971
+ const defaultProfile = isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
32493
32972
  if (executionProfileOverride) Object.assign(defaultProfile, executionProfileOverride);
32494
32973
  if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
32495
32974
  else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
@@ -32563,9 +33042,13 @@ const ZedToolPermissionsSchema = zod_mini.z.looseObject({
32563
33042
  default: zod_mini.z.optional(ZedPermissionActionSchema),
32564
33043
  tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), ZedToolPermissionSchema))
32565
33044
  });
33045
+ /** Canonical per-tool MCP category prefix: `mcp__<server>__<tool>`. */
33046
+ const MCP_CANONICAL_PREFIX = "mcp__";
33047
+ /** Zed's per-tool MCP name prefix: `mcp:<server>:<tool>`. */
33048
+ const MCP_ZED_PREFIX = "mcp:";
32566
33049
  /**
32567
33050
  * Mapping from rulesync canonical tool category names to Zed agent tool names.
32568
- * Unknown names are passed through as-is (e.g. `mcp:<server>:<tool>` keys).
33051
+ * Unknown names are passed through as-is.
32569
33052
  */
32570
33053
  const CANONICAL_TO_ZED_TOOL_NAMES = {
32571
33054
  bash: "terminal",
@@ -32600,12 +33083,68 @@ const ZED_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
32600
33083
  ]);
32601
33084
  const isZedExcludedCategory = (category) => ZED_EXCLUDED_CANONICAL_CATEGORIES.has(category) || ZED_EXCLUDED_TOOL_NAMES.has(toZedToolName(category));
32602
33085
  const ZED_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZED_TOOL_NAMES).map(([k, v]) => [v, k]));
33086
+ /**
33087
+ * Zed addresses an MCP tool as `mcp:<server>:<tool>`, where rulesync's canonical
33088
+ * category is `mcp__<server>__<tool>`. Without this translation the canonical
33089
+ * spelling lands under a key Zed never looks up.
33090
+ *
33091
+ * Only the FIRST separator is split, matching the `cursor-permissions.ts`
33092
+ * precedent: upstream's `mcp_tool_id` concatenates the two names without
33093
+ * escaping either, so a tool called `create__issue` is legitimately
33094
+ * `mcp:github:create__issue`. Splitting every separator would rewrite that into
33095
+ * a third key on the next generate.
33096
+ *
33097
+ * @see https://zed.dev/docs/ai/tool-permissions
33098
+ */
32603
33099
  function toZedToolName(canonical) {
33100
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX)) {
33101
+ const [server, ...toolParts] = canonical.slice(5).split("__");
33102
+ const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
33103
+ return `${MCP_ZED_PREFIX}${address}`;
33104
+ }
32604
33105
  return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
32605
33106
  }
32606
33107
  function toCanonicalToolName(zedName) {
33108
+ if (zedName.startsWith(MCP_ZED_PREFIX)) {
33109
+ const [server, ...toolParts] = zedName.slice(4).split(":");
33110
+ const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
33111
+ return `${MCP_CANONICAL_PREFIX}${address}`;
33112
+ }
32607
33113
  return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
32608
33114
  }
33115
+ /**
33116
+ * Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
33117
+ * tool's text input, and it dispatches every MCP tool with a single empty input
33118
+ * (`&[String::new()]`, commented upstream as "MCP tools are gated only by tool
33119
+ * id (no per-input pattern matching)"). The regexes still run, but against `""`,
33120
+ * so a pattern-scoped rule silently does something other than what its author
33121
+ * meant — and a non-matching `always_allow` downgrades the outcome to confirm.
33122
+ * Only the category's `*` rule (Zed's per-tool `default`) is therefore emitted.
33123
+ */
33124
+ const isMcpZedToolName = (zedName) => zedName.startsWith(MCP_ZED_PREFIX);
33125
+ /**
33126
+ * Zed looks a tool up by exact key on the full `mcp:<server>:<tool>` triple, so
33127
+ * an address is inert unless it names both a concrete server and a concrete
33128
+ * tool: a missing half matches nothing, and so does a wildcard, since Zed does
33129
+ * no glob or prefix matching on the key.
33130
+ */
33131
+ function isInertMcpAddress(zedToolName) {
33132
+ if (!isMcpZedToolName(zedToolName)) return false;
33133
+ const [server, ...toolParts] = zedToolName.slice(4).split(":");
33134
+ const tool = toolParts.join(":");
33135
+ return !server || server === "*" || !tool || tool === "*";
33136
+ }
33137
+ /**
33138
+ * Strip pattern-scoped rules from an MCP category, warning once about the ones
33139
+ * dropped. Non-MCP categories are returned untouched.
33140
+ */
33141
+ function withoutInertMcpPatterns({ category, zedToolName, rules, logger }) {
33142
+ if (!isMcpZedToolName(zedToolName)) return rules;
33143
+ const scopedPatterns = Object.keys(rules).filter((pattern) => pattern !== "*");
33144
+ if (scopedPatterns.length === 0) return rules;
33145
+ logger?.warn(`Zed permissions: dropping the pattern-scoped rule(s) ${scopedPatterns.map((pattern) => `"${pattern}"`).join(", ")} in the "${category}" category — Zed dispatches an MCP tool with an empty input, so a permission pattern is matched against "" rather than against anything meaningful. Only the catch-all "*" rule is emitted, as the tool's default.`);
33146
+ return Object.fromEntries(Object.entries(rules).filter(([pattern]) => pattern === "*"));
33147
+ }
32609
33148
  const CANONICAL_TO_ZED_ACTION = {
32610
33149
  allow: "allow",
32611
33150
  ask: "confirm",
@@ -32648,6 +33187,53 @@ function buildZedToolPermission(rules) {
32648
33187
  if (alwaysConfirm.length > 0) tool.always_confirm = alwaysConfirm;
32649
33188
  return Object.keys(tool).length > 0 ? tool : null;
32650
33189
  }
33190
+ /**
33191
+ * Split a canonical permission block into the Zed shapes it maps onto, plus the
33192
+ * categories the caller should report as dropped.
33193
+ *
33194
+ * The canonical `*` category is the all-tools catch-all. Zed's counterpart is
33195
+ * `agent.tool_permissions.default` (rung 6 of its precedence ladder), not a
33196
+ * `tools["*"]` entry — `*` is not a Zed tool name, so writing one produces a
33197
+ * rule Zed silently ignores. Only the category's own `*` pattern can be
33198
+ * expressed there: Zed's global default carries no pattern list, so
33199
+ * pattern-scoped rules in the `*` category are dropped with a warning instead of
33200
+ * being emitted as inert config.
33201
+ */
33202
+ function buildZedToolPermissions({ permission, logger }) {
33203
+ let managedDefault;
33204
+ const managedTools = {};
33205
+ const excludedCategories = [];
33206
+ const inertMcpCategories = [];
33207
+ for (const [category, rules] of Object.entries(permission)) {
33208
+ if (category === "*") {
33209
+ for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
33210
+ else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
33211
+ continue;
33212
+ }
33213
+ if (isZedExcludedCategory(category)) {
33214
+ if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
33215
+ continue;
33216
+ }
33217
+ const zedToolName = toZedToolName(category);
33218
+ if (isInertMcpAddress(zedToolName)) {
33219
+ inertMcpCategories.push(category);
33220
+ continue;
33221
+ }
33222
+ const tool = buildZedToolPermission(withoutInertMcpPatterns({
33223
+ category,
33224
+ zedToolName,
33225
+ rules,
33226
+ logger
33227
+ }));
33228
+ if (tool) managedTools[zedToolName] = tool;
33229
+ }
33230
+ return {
33231
+ managedDefault,
33232
+ managedTools,
33233
+ excludedCategories,
33234
+ inertMcpCategories
33235
+ };
33236
+ }
32651
33237
  function asRecord(value) {
32652
33238
  if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
32653
33239
  return Object.fromEntries(Object.entries(value));
@@ -32718,25 +33304,15 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
32718
33304
  const agent = asRecord(settings.agent);
32719
33305
  const toolPermissions = asRecord(agent.tool_permissions);
32720
33306
  const existingTools = asRecord(toolPermissions.tools);
32721
- let managedDefault;
32722
- const managedTools = {};
32723
- const excludedCategories = [];
32724
- for (const [category, rules] of Object.entries(config.permission)) {
32725
- if (category === "*") {
32726
- for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
32727
- else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
32728
- continue;
32729
- }
32730
- if (isZedExcludedCategory(category)) {
32731
- if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
32732
- continue;
32733
- }
32734
- const tool = buildZedToolPermission(rules);
32735
- if (tool) managedTools[toZedToolName(category)] = tool;
32736
- }
33307
+ const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
33308
+ permission: config.permission,
33309
+ logger
33310
+ });
32737
33311
  if (excludedCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${excludedCategories.map((category) => `"${category}"`).join(", ")} ${excludedCategories.length === 1 ? "category" : "categories"} — Zed does not gate its read-only tools, so the entries would never be consulted. Zed's read-denial surface is \`private_files\`, which the ignore feature writes from \`.rulesync/.aiignore\`.`);
33312
+ if (inertMcpCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${inertMcpCategories.map((category) => `"${category}"`).join(", ")} ${inertMcpCategories.length === 1 ? "category" : "categories"} — Zed looks an MCP tool up by exact key on the full \`mcp:<server>:<tool>\` triple, so an address that omits or wildcards either half matches nothing. Name the individual server and tool instead.`);
32738
33313
  const managedToolNames = new Set(Object.keys(managedTools));
32739
33314
  if ("*" in config.permission) managedToolNames.add("*");
33315
+ for (const toolName of Object.keys(existingTools)) if (toolName.startsWith(MCP_CANONICAL_PREFIX)) managedToolNames.add(toolName);
32740
33316
  const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
32741
33317
  return new ZedPermissions({
32742
33318
  outputRoot,
@@ -34937,18 +35513,10 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
34937
35513
  };
34938
35514
  }
34939
35515
  toRulesyncSkill() {
34940
- const frontmatter = this.getFrontmatter();
34941
- const copilotSection = {
34942
- ...frontmatter.license !== void 0 && { license: frontmatter.license },
34943
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
34944
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
34945
- ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
34946
- ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
34947
- ...frontmatter.context !== void 0 && { context: frontmatter.context }
34948
- };
35516
+ const { name, description, ...copilotSection } = this.getFrontmatter();
34949
35517
  const rulesyncFrontmatter = {
34950
- name: frontmatter.name,
34951
- description: frontmatter.description,
35518
+ name,
35519
+ description,
34952
35520
  targets: ["*"],
34953
35521
  ...Object.keys(copilotSection).length > 0 && { copilot: copilotSection }
34954
35522
  };
@@ -34975,15 +35543,13 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
34975
35543
  rootFrontmatter: rulesyncFrontmatter,
34976
35544
  section: copilotSection
34977
35545
  });
35546
+ const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotFields } = copilotSection ?? {};
34978
35547
  const copilotFrontmatter = {
35548
+ ...copilotFields,
34979
35549
  name: rulesyncFrontmatter.name,
34980
35550
  description: rulesyncFrontmatter.description,
34981
- ...copilotSection?.license !== void 0 && { license: copilotSection.license },
34982
- ...copilotSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotSection["allowed-tools"] },
34983
- ...copilotSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotSection["argument-hint"] },
34984
35551
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
34985
- ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
34986
- ...copilotSection?.context !== void 0 && { context: copilotSection.context }
35552
+ ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
34987
35553
  };
34988
35554
  return new CopilotSkill({
34989
35555
  outputRoot,
@@ -35102,17 +35668,10 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
35102
35668
  };
35103
35669
  }
35104
35670
  toRulesyncSkill() {
35105
- const frontmatter = this.getFrontmatter();
35106
- const copilotcliSection = {
35107
- ...frontmatter.license !== void 0 && { license: frontmatter.license },
35108
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
35109
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
35110
- ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
35111
- ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
35112
- };
35671
+ const { name, description, ...copilotcliSection } = this.getFrontmatter();
35113
35672
  const rulesyncFrontmatter = {
35114
- name: frontmatter.name,
35115
- description: frontmatter.description,
35673
+ name,
35674
+ description,
35116
35675
  targets: ["*"],
35117
35676
  ...Object.keys(copilotcliSection).length > 0 && { copilotcli: copilotcliSection }
35118
35677
  };
@@ -35139,12 +35698,11 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
35139
35698
  rootFrontmatter: rulesyncFrontmatter,
35140
35699
  section: copilotcliSection
35141
35700
  });
35701
+ const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotcliFields } = copilotcliSection ?? {};
35142
35702
  const copilotcliFrontmatter = {
35703
+ ...copilotcliFields,
35143
35704
  name: rulesyncFrontmatter.name,
35144
35705
  description: rulesyncFrontmatter.description,
35145
- ...copilotcliSection?.license !== void 0 && { license: copilotcliSection.license },
35146
- ...copilotcliSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotcliSection["allowed-tools"] },
35147
- ...copilotcliSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotcliSection["argument-hint"] },
35148
35706
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
35149
35707
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
35150
35708
  };
@@ -36618,7 +37176,10 @@ var KimiCodeSkill = class KimiCodeSkill extends ToolSkill {
36618
37176
  //#region src/features/skills/kiro-skill.ts
36619
37177
  const KiroSkillFrontmatterSchema = zod_mini.z.looseObject({
36620
37178
  name: zod_mini.z.string(),
36621
- description: zod_mini.z.string()
37179
+ description: zod_mini.z.string(),
37180
+ license: zod_mini.z.optional(zod_mini.z.string()),
37181
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
37182
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
36622
37183
  });
36623
37184
  /**
36624
37185
  * Represents a Kiro skill directory.
@@ -36672,11 +37233,12 @@ var KiroSkill = class KiroSkill extends ToolSkill {
36672
37233
  };
36673
37234
  }
36674
37235
  toRulesyncSkill() {
36675
- const frontmatter = this.getFrontmatter();
37236
+ const { name, description, ...kiroSection } = this.getFrontmatter();
36676
37237
  const rulesyncFrontmatter = {
36677
- name: frontmatter.name,
36678
- description: frontmatter.description,
36679
- targets: ["*"]
37238
+ name,
37239
+ description,
37240
+ targets: ["*"],
37241
+ ...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
36680
37242
  };
36681
37243
  return new RulesyncSkill({
36682
37244
  outputRoot: this.outputRoot,
@@ -36693,6 +37255,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
36693
37255
  const settablePaths = KiroSkill.getSettablePaths({ global });
36694
37256
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
36695
37257
  const kiroFrontmatter = {
37258
+ ...rulesyncFrontmatter.kiro,
36696
37259
  name: rulesyncFrontmatter.name,
36697
37260
  description: rulesyncFrontmatter.description
36698
37261
  };
@@ -43261,14 +43824,14 @@ var ZoocodeSubagent = class extends RooSubagent {
43261
43824
  static toRooMode(rulesyncSubagent) {
43262
43825
  const mode = super.toRooMode(rulesyncSubagent);
43263
43826
  const frontmatter = rulesyncSubagent.getFrontmatter();
43264
- const zoocodeSection = isRecord(frontmatter.zoocode) ? frontmatter.zoocode : {};
43827
+ const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
43265
43828
  if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
43266
43829
  return mode;
43267
43830
  }
43268
43831
  toRulesyncSubagents() {
43269
43832
  return super.toRulesyncSubagents().map((subagent) => {
43270
43833
  const frontmatter = subagent.getFrontmatter();
43271
- const { allowedMcpServers, ...restRooSection } = isRecord(frontmatter.roo) ? { ...frontmatter.roo } : {};
43834
+ const { allowedMcpServers, ...restRooSection } = isRecord$1(frontmatter.roo) ? { ...frontmatter.roo } : {};
43272
43835
  const rebuilt = {
43273
43836
  ...frontmatter,
43274
43837
  targets: ["zoocode"],