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.
@@ -1842,7 +1842,7 @@ var RulesyncFile = class extends AiFile {
1842
1842
  * Type guard to check if a value is a plain object (Record<string, unknown>).
1843
1843
  * This excludes arrays and null values.
1844
1844
  */
1845
- function isRecord(value) {
1845
+ function isRecord$1(value) {
1846
1846
  return typeof value === "object" && value !== null && !Array.isArray(value);
1847
1847
  }
1848
1848
  /**
@@ -1857,7 +1857,7 @@ function isRecord(value) {
1857
1857
  * malicious accessor descriptors.
1858
1858
  */
1859
1859
  function isPlainObject$1(value) {
1860
- if (!isRecord(value)) return false;
1860
+ if (!isRecord$1(value)) return false;
1861
1861
  const proto = Object.getPrototypeOf(value);
1862
1862
  return proto === null || proto === Object.prototype;
1863
1863
  }
@@ -2163,6 +2163,7 @@ const HookDefinitionSchema = z.looseObject({
2163
2163
  timeout: z.optional(z.number()),
2164
2164
  cacheTtl: z.optional(z.number().check(nonnegative())),
2165
2165
  matcher: z.optional(safeString),
2166
+ enabled: z.optional(z.boolean()),
2166
2167
  prompt: z.optional(safeString),
2167
2168
  loop_limit: z.optional(z.nullable(z.number())),
2168
2169
  name: z.optional(safeString),
@@ -2481,8 +2482,9 @@ const FACTORYDROID_HOOK_EVENTS = [
2481
2482
  /**
2482
2483
  * Hook events supported by deepagents-cli (`deepagents-code` / `dcode`).
2483
2484
  *
2484
- * The canonical `notification` event maps to dcode's `input.required`
2485
- * (human-in-the-loop interrupt) the closest documented equivalent.
2485
+ * These are the twelve Hooks v2 `HookEvent` members, GA since deepagents-code
2486
+ * 0.1.52. Canonical `contextOffload` is deliberately absent — see
2487
+ * {@link CANONICAL_TO_DEEPAGENTS_EVENT_NAMES}.
2486
2488
  * https://docs.langchain.com/oss/python/deepagents/cli/configuration
2487
2489
  */
2488
2490
  const DEEPAGENTS_HOOK_EVENTS = [
@@ -2495,8 +2497,9 @@ const DEEPAGENTS_HOOK_EVENTS = [
2495
2497
  "postToolUseFailure",
2496
2498
  "stop",
2497
2499
  "preCompact",
2498
- "contextOffload",
2499
- "notification"
2500
+ "notification",
2501
+ "subagentStart",
2502
+ "subagentStop"
2500
2503
  ];
2501
2504
  /** Hook events supported by Codex CLI. */
2502
2505
  const CODEXCLI_HOOK_EVENTS = [
@@ -3184,26 +3187,59 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
3184
3187
  */
3185
3188
  const GOOSE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GOOSE_EVENT_NAMES).map(([k, v]) => [v, k]));
3186
3189
  /**
3187
- * Map canonical camelCase event names to deepagents-cli dot-notation.
3190
+ * Map canonical camelCase event names to the deepagents-cli Hooks v2
3191
+ * `HookEvent` values.
3192
+ *
3193
+ * Hooks v2 went GA in deepagents-code 0.1.52 (2026-08-04) and replaced the
3194
+ * legacy dot-notation names (`session.start`, `tool.use`, …) with these twelve
3195
+ * PascalCase members. The legacy list format is still read, but is scheduled for
3196
+ * removal on 2026-09-01 (`_LEGACY_HOOKS_REMOVAL_DATE` in `hooks/loading.py`).
3197
+ *
3198
+ * Canonical `contextOffload` has no v2 counterpart. Its legacy event
3199
+ * (`context.offload`) is gone, and folding it onto `PreCompact` would silently
3200
+ * merge two distinct canonical events into one — so it is dropped for
3201
+ * deepagents instead, and reported by the hooks processor as an unsupported
3202
+ * event like any other.
3203
+ *
3204
+ * @see https://github.com/langchain-ai/deepagents `libs/code/deepagents_code/hooks/models/domain.py`
3188
3205
  */
3189
3206
  const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
3190
- sessionStart: "session.start",
3191
- sessionEnd: "session.end",
3192
- beforeSubmitPrompt: "user.prompt",
3193
- permissionRequest: "permission.request",
3194
- preToolUse: "tool.use",
3195
- postToolUse: "tool.result",
3196
- postToolUseFailure: "tool.error",
3197
- stop: "task.complete",
3198
- preCompact: "context.compact",
3199
- contextOffload: "context.offload",
3200
- notification: "input.required"
3207
+ sessionStart: "SessionStart",
3208
+ beforeSubmitPrompt: "UserPromptSubmit",
3209
+ sessionEnd: "SessionEnd",
3210
+ permissionRequest: "PermissionRequest",
3211
+ notification: "Notification",
3212
+ preToolUse: "PreToolUse",
3213
+ postToolUse: "PostToolUse",
3214
+ postToolUseFailure: "PostToolUseFailure",
3215
+ preCompact: "PreCompact",
3216
+ stop: "Stop",
3217
+ subagentStart: "SubagentStart",
3218
+ subagentStop: "SubagentStop"
3201
3219
  };
3202
3220
  /**
3203
- * Map deepagents-cli dot-notation event names to canonical camelCase.
3221
+ * Map deepagents-cli `HookEvent` values to canonical camelCase.
3204
3222
  */
3205
3223
  const DEEPAGENTS_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEEPAGENTS_EVENT_NAMES).map(([k, v]) => [v, k]));
3206
3224
  /**
3225
+ * The legacy dot-notation event names deepagents-cli used before Hooks v2.
3226
+ * Kept for the read-only import path so a `hooks.json` still in the old format
3227
+ * round-trips into canonical events instead of being silently discarded.
3228
+ */
3229
+ const DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES = {
3230
+ "session.start": "sessionStart",
3231
+ "session.end": "sessionEnd",
3232
+ "user.prompt": "beforeSubmitPrompt",
3233
+ "permission.request": "permissionRequest",
3234
+ "tool.use": "preToolUse",
3235
+ "tool.result": "postToolUse",
3236
+ "tool.error": "postToolUseFailure",
3237
+ "task.complete": "stop",
3238
+ "context.compact": "preCompact",
3239
+ "context.offload": "contextOffload",
3240
+ "input.required": "notification"
3241
+ };
3242
+ /**
3207
3243
  * Map canonical camelCase event names to Kiro CLI camelCase.
3208
3244
  * Kiro CLI uses its own event naming: agentSpawn, userPromptSubmit, preToolUse,
3209
3245
  * postToolUse, stop. Both `sessionEnd` and `stop` canonical events map to
@@ -3821,7 +3857,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3821
3857
  logger
3822
3858
  });
3823
3859
  for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
3824
- if (!isRecord(json[ignoredKey])) continue;
3860
+ if (!isRecord$1(json[ignoredKey])) continue;
3825
3861
  this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
3826
3862
  }
3827
3863
  const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
@@ -3833,7 +3869,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3833
3869
  }));
3834
3870
  for (const blockKey of blockKeys) {
3835
3871
  const toolBlock = json[blockKey];
3836
- const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
3872
+ const toolServers = isRecord$1(toolBlock) && isRecord$1(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
3837
3873
  for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
3838
3874
  if (isPrototypePollutionKey(serverName)) continue;
3839
3875
  if (serverConfig === null) delete effectiveServers[serverName];
@@ -4850,9 +4886,9 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
4850
4886
  if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
4851
4887
  const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
4852
4888
  const json = this.json;
4853
- if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
4889
+ if (overrideKey !== toolTarget && isRecord$1(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
4854
4890
  const overrideBlock = json[overrideKey];
4855
- if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
4891
+ if (!isRecord$1(overrideBlock) || !isRecord$1(overrideBlock.permission)) return this;
4856
4892
  const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
4857
4893
  const merged = {
4858
4894
  ...json,
@@ -5162,6 +5198,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
5162
5198
  compatibility: z.optional(z.looseObject({})),
5163
5199
  metadata: z.optional(z.looseObject({}))
5164
5200
  })),
5201
+ kiro: z.optional(z.looseObject({
5202
+ license: z.optional(z.string()),
5203
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
5204
+ metadata: z.optional(z.looseObject({}))
5205
+ })),
5165
5206
  deepagents: z.optional(z.looseObject({
5166
5207
  "allowed-tools": z.optional(z.array(z.string())),
5167
5208
  license: z.optional(z.string()),
@@ -8256,7 +8297,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
8256
8297
  }
8257
8298
  static extractAntigravityConfig(rulesyncCommand) {
8258
8299
  const antigravity = rulesyncCommand.getFrontmatter().antigravity;
8259
- return isRecord(antigravity) ? antigravity : void 0;
8300
+ return isRecord$1(antigravity) ? antigravity : void 0;
8260
8301
  }
8261
8302
  static resolveTrigger(rulesyncCommand, antigravityConfig) {
8262
8303
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
@@ -12046,8 +12087,8 @@ async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
12046
12087
  }
12047
12088
  if (!isPlainObject$1(parsed) || !Array.isArray(parsed.prompts)) return "";
12048
12089
  const expectedContentFile = toPosixPath(join("prompts", relativeFilePath));
12049
- const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
12050
- return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
12090
+ const entry = parsed.prompts.find((candidate) => isRecord$1(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
12091
+ return isRecord$1(entry) && typeof entry.description === "string" ? entry.description : "";
12051
12092
  }
12052
12093
  /**
12053
12094
  * The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
@@ -13160,97 +13201,140 @@ function applyCommandPrefix({ def, converterConfig }) {
13160
13201
  return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
13161
13202
  }
13162
13203
  /**
13163
- * Emit the configured boolean passthrough fields on the tool side, mapping each
13164
- * canonical field name to its (possibly renamed) tool field name. Only boolean
13165
- * values are carried through.
13166
- */
13167
- function emitBooleanPassthroughFields({ def, hookType, converterConfig }) {
13168
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13169
- if (commandOnly === true && hookType !== "command") return false;
13170
- return typeof def[canonical] === "boolean";
13171
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13172
- }
13173
- /**
13174
- * Import the configured boolean passthrough fields back into canonical fields,
13175
- * reversing {@link emitBooleanPassthroughFields}. Only boolean values are read.
13176
- */
13177
- function importBooleanPassthroughFields({ h, converterConfig }) {
13178
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "boolean").map(({ canonical, tool }) => [canonical, h[tool]]));
13179
- }
13180
- /**
13181
- * Emit the configured number passthrough fields on the tool side, mapping each
13182
- * canonical field name to its (possibly renamed) tool field name. Only finite
13183
- * numbers are carried through.
13184
- */
13185
- function emitNumberPassthroughFields({ def, hookType, converterConfig }) {
13186
- return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13187
- if (commandOnly === true && hookType !== "command") return false;
13188
- return Number.isFinite(def[canonical]);
13189
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13190
- }
13191
- /**
13192
- * Import the configured number passthrough fields back into canonical fields,
13193
- * reversing {@link emitNumberPassthroughFields}. Only finite numbers are read.
13194
- */
13195
- function importNumberPassthroughFields({ h, converterConfig }) {
13196
- return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ tool }) => Number.isFinite(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
13197
- }
13198
- /**
13199
- * Emit the configured string passthrough fields on the tool side, mapping each
13200
- * canonical field name to its (possibly renamed) tool field name. Only non-empty
13201
- * string values are carried through.
13202
- */
13203
- function emitStringPassthroughFields({ def, hookType, converterConfig }) {
13204
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13205
- if (commandOnly === true && hookType !== "command") return false;
13206
- return typeof def[canonical] === "string" && def[canonical] !== "";
13207
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13208
- }
13209
- /**
13210
- * Import the configured string passthrough fields back into canonical fields,
13211
- * reversing {@link emitStringPassthroughFields}. Only non-empty string values
13212
- * are read.
13213
- */
13214
- function importStringPassthroughFields({ h, converterConfig }) {
13215
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "string" && h[tool] !== "").map(({ canonical, tool }) => [canonical, h[tool]]));
13216
- }
13217
- /**
13218
- * Emit the configured string-array passthrough fields on the tool side.
13219
- */
13220
- function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
13221
- return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13222
- if (commandOnly === true && hookType !== "command") return false;
13223
- return isStringArray(def[canonical]);
13224
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13225
- }
13226
- /**
13227
- * Import the configured string-array passthrough fields, reversing
13228
- * {@link emitArrayPassthroughFields}.
13204
+ * Whether a field registered for `command` hooks only applies to this hook.
13205
+ * Applied on both export and import: a value imported into a canonical field
13206
+ * the exporter would then drop is silently deleted on the next generate.
13229
13207
  */
13230
- function importArrayPassthroughFields({ h, converterConfig, logger }) {
13231
- const fields = converterConfig.arrayPassthroughFields ?? [];
13232
- 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.`);
13233
- return Object.fromEntries(fields.filter(({ tool }) => isSafeStringArray(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
13208
+ function isFieldApplicable({ commandOnly, hookType }) {
13209
+ return commandOnly !== true || hookType === "command";
13234
13210
  }
13235
13211
  /**
13236
- * Emit the configured string-map passthrough fields on the tool side. Only maps
13237
- * whose values are all control-character-free strings are carried through.
13212
+ * Emit the configured passthrough fields on the tool side, mapping each
13213
+ * canonical field name to its (possibly renamed) tool field name. Only values
13214
+ * accepted by `isValid` are carried through, so a malformed field can't leak
13215
+ * into a config the tool would reject.
13216
+ *
13217
+ * A field the tool documents on `command` hooks only is dropped when authored
13218
+ * on another hook type. That is a value the user hand-wrote in
13219
+ * `.rulesync/hooks.*` and the canonical schema does not cross-validate it
13220
+ * against the hook's type, so it is warned about rather than deleted in
13221
+ * silence — the mirror of the import side.
13238
13222
  */
13239
- function emitRecordPassthroughFields({ def, hookType, converterConfig }) {
13240
- return Object.fromEntries((converterConfig.recordPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
13241
- if (commandOnly === true && hookType !== "command") return false;
13242
- return isSafeStringRecord(def[canonical]);
13243
- }).map(({ canonical, tool }) => [tool, def[canonical]]));
13244
- }
13223
+ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn }) {
13224
+ for (const { canonical, tool, commandOnly } of fields) {
13225
+ const value = def[canonical];
13226
+ if (value === void 0) continue;
13227
+ if (!isFieldApplicable({
13228
+ commandOnly,
13229
+ hookType
13230
+ })) {
13231
+ warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not generated.`);
13232
+ continue;
13233
+ }
13234
+ if (!isValid({
13235
+ value,
13236
+ canonical
13237
+ })) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${JSON.stringify(value)} is not a value this tool can express as "${tool}".`);
13238
+ }
13239
+ return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
13240
+ commandOnly,
13241
+ hookType
13242
+ }) && isValid({
13243
+ value: def[canonical],
13244
+ canonical
13245
+ })).map(({ canonical, tool }) => [tool, def[canonical]]));
13246
+ }
13247
+ /**
13248
+ * Import the configured passthrough fields back into canonical fields,
13249
+ * reversing {@link emitPassthroughFields}. A field the tool documents on
13250
+ * `command` hooks only is skipped here too, and `describeInvalid` — when the
13251
+ * kind has a rule an authored file can plausibly violate — turns a rejected
13252
+ * value into a warning instead of a silent drop.
13253
+ */
13254
+ function importPassthroughFields({ h, hookType, fields, isValid, describeInvalid, warn }) {
13255
+ const applicable = fields.filter(({ commandOnly }) => isFieldApplicable({
13256
+ commandOnly,
13257
+ hookType
13258
+ }));
13259
+ const skipped = fields.filter(({ commandOnly }) => !isFieldApplicable({
13260
+ commandOnly,
13261
+ hookType
13262
+ }));
13263
+ 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.`);
13264
+ for (const { tool, canonical } of applicable) if (describeInvalid !== void 0 && h[tool] !== void 0 && !isValid({
13265
+ value: h[tool],
13266
+ canonical
13267
+ })) warn?.(describeInvalid({
13268
+ tool,
13269
+ canonical,
13270
+ value: h[tool]
13271
+ }));
13272
+ return Object.fromEntries(applicable.filter(({ tool, canonical }) => isValid({
13273
+ value: h[tool],
13274
+ canonical
13275
+ })).map(({ canonical, tool }) => [canonical, h[tool]]));
13276
+ }
13277
+ const isBooleanValue = ({ value }) => typeof value === "boolean";
13278
+ const isFiniteNumber = ({ value }) => Number.isFinite(value);
13279
+ const isNonEmptyString = (value) => typeof value === "string" && value !== "";
13280
+ const isEmittableString = ({ value }) => isNonEmptyString(value);
13281
+ const isEmittableArray = ({ value }) => isStringArray(value);
13282
+ const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
13283
+ const isImportableArray = ({ value }) => isSafeStringArray(value);
13284
+ /**
13285
+ * The canonical schema of one hook field, looked up by name. Read off the
13286
+ * schema's own shape rather than by parsing a one-field object, so a `canonical`
13287
+ * name that no longer exists resolves to `undefined` (a `looseObject` would
13288
+ * accept an unknown key and silently validate nothing) and a typo is caught by
13289
+ * the tests instead of quietly disabling the check.
13290
+ */
13291
+ const CANONICAL_FIELD_SCHEMAS = HookDefinitionSchema.def.shape;
13292
+ /**
13293
+ * Whether a value satisfies the constraints the canonical schema puts on the
13294
+ * field it would be imported into.
13295
+ *
13296
+ * Import needs this on top of the kind's shape check: a hand-written tool
13297
+ * settings file can hold `"shell": "zsh"`, which is a non-empty string but not
13298
+ * a member of the canonical `z.enum(["bash", "powershell"])`. Letting it in
13299
+ * would write a `.rulesync/hooks.jsonc` that fails validation on the *next*
13300
+ * run, taking the whole hooks feature down with it.
13301
+ */
13302
+ function satisfiesCanonicalField({ value, canonical }) {
13303
+ const schema = canonicalFieldSchema(canonical);
13304
+ return schema !== void 0 && z.safeParse(schema, value).success;
13305
+ }
13306
+ /** Own properties only, so a name like `toString` resolves to nothing. */
13307
+ function canonicalFieldSchema(canonical) {
13308
+ return Object.hasOwn(CANONICAL_FIELD_SCHEMAS, canonical) ? CANONICAL_FIELD_SCHEMAS[canonical] : void 0;
13309
+ }
13310
+ const isImportableString = ({ value, canonical }) => isNonEmptyString(value) && satisfiesCanonicalField({
13311
+ value,
13312
+ canonical
13313
+ });
13314
+ const isImportableNumber = ({ value, canonical }) => Number.isFinite(value) && satisfiesCanonicalField({
13315
+ value,
13316
+ canonical
13317
+ });
13245
13318
  /**
13246
- * Import the configured string-map passthrough fields, reversing
13247
- * {@link emitRecordPassthroughFields}.
13248
- */
13249
- function importRecordPassthroughFields({ h, hookType, converterConfig, logger }) {
13250
- const fields = (converterConfig.recordPassthroughFields ?? []).filter(({ commandOnly }) => commandOnly !== true || hookType === "command");
13251
- 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.`);
13252
- return Object.fromEntries(fields.filter(({ tool }) => isSafeStringRecord(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
13253
- }
13319
+ * Say which rule the value broke, so the warning names the actual constraint
13320
+ * rather than asserting a canonical rejection that may not be the reason. A
13321
+ * closed enum lists its members; a rule carrying its own message (the
13322
+ * control-character check behind `safeString`) reuses it.
13323
+ */
13324
+ function describeScalarConstraint({ canonical, value }) {
13325
+ const schema = canonicalFieldSchema(canonical);
13326
+ const result = schema === void 0 ? void 0 : z.safeParse(schema, value);
13327
+ if (result === void 0 || result.success) return `it is not a value this field carries through.`;
13328
+ const issue = result.error.issues[0];
13329
+ if (issue === void 0) return `it is not a value the canonical "${canonical}" field accepts.`;
13330
+ return `it does not satisfy the canonical "${canonical}" field: ${issue.message}.`;
13331
+ }
13332
+ const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${JSON.stringify(value)}) while importing a hook: ${describeScalarConstraint({
13333
+ canonical,
13334
+ value
13335
+ })} Importing it would fail validation on the next run.`;
13336
+ const describeInvalidArray = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a list of strings without newline, carriage return or NUL characters.`;
13337
+ 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.`;
13254
13338
  /**
13255
13339
  * Check a value against the shape its field documents. A string field also
13256
13340
  * rejects control characters, matching the canonical `safeString` so an
@@ -13311,7 +13395,54 @@ function emitTypePayloadFields({ def, hookType, converterConfig }) {
13311
13395
  function isSupportedHookType({ type, converterConfig }) {
13312
13396
  return converterConfig.supportedHookTypes?.has(type ?? "command") ?? true;
13313
13397
  }
13314
- function buildToolHooks({ defs, converterConfig }) {
13398
+ /**
13399
+ * Emit every per-hook passthrough kind for one canonical definition.
13400
+ */
13401
+ function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, warn }) {
13402
+ return {
13403
+ ...emitPassthroughFields({
13404
+ def,
13405
+ hookType,
13406
+ eventName,
13407
+ fields: converterConfig.booleanPassthroughFields ?? [],
13408
+ isValid: isBooleanValue,
13409
+ warn
13410
+ }),
13411
+ ...emitPassthroughFields({
13412
+ def,
13413
+ hookType,
13414
+ eventName,
13415
+ fields: converterConfig.numberPassthroughFields ?? [],
13416
+ isValid: isFiniteNumber,
13417
+ warn
13418
+ }),
13419
+ ...emitPassthroughFields({
13420
+ def,
13421
+ hookType,
13422
+ eventName,
13423
+ fields: converterConfig.stringPassthroughFields ?? [],
13424
+ isValid: isEmittableString,
13425
+ warn
13426
+ }),
13427
+ ...emitPassthroughFields({
13428
+ def,
13429
+ hookType,
13430
+ eventName,
13431
+ fields: converterConfig.arrayPassthroughFields ?? [],
13432
+ isValid: isEmittableArray,
13433
+ warn
13434
+ }),
13435
+ ...emitPassthroughFields({
13436
+ def,
13437
+ hookType,
13438
+ eventName,
13439
+ fields: converterConfig.recordPassthroughFields ?? [],
13440
+ isValid: isEmittableRecord,
13441
+ warn
13442
+ })
13443
+ };
13444
+ }
13445
+ function buildToolHooks({ defs, eventName, converterConfig, warn }) {
13315
13446
  const hooks = [];
13316
13447
  for (const def of defs) {
13317
13448
  const hookType = def.type ?? "command";
@@ -13324,30 +13455,12 @@ function buildToolHooks({ defs, converterConfig }) {
13324
13455
  converterConfig
13325
13456
  });
13326
13457
  hooks.push({
13327
- ...emitBooleanPassthroughFields({
13328
- def,
13329
- hookType,
13330
- converterConfig
13331
- }),
13332
- ...emitNumberPassthroughFields({
13333
- def,
13334
- hookType,
13335
- converterConfig
13336
- }),
13337
- ...emitStringPassthroughFields({
13338
- def,
13339
- hookType,
13340
- converterConfig
13341
- }),
13342
- ...emitArrayPassthroughFields({
13458
+ ...emitAllPassthroughFields({
13343
13459
  def,
13344
13460
  hookType,
13345
- converterConfig
13346
- }),
13347
- ...emitRecordPassthroughFields({
13348
- def,
13349
- hookType,
13350
- converterConfig
13461
+ eventName,
13462
+ converterConfig,
13463
+ warn
13351
13464
  }),
13352
13465
  type: hookType,
13353
13466
  ...command !== void 0 && command !== null && { command },
@@ -13365,6 +13478,17 @@ function buildToolHooks({ defs, converterConfig }) {
13365
13478
  return hooks;
13366
13479
  }
13367
13480
  /**
13481
+ * A `warn` that says each distinct thing once per conversion.
13482
+ */
13483
+ function warnOnce(logger) {
13484
+ const seen = /* @__PURE__ */ new Set();
13485
+ return (message) => {
13486
+ if (seen.has(message)) return;
13487
+ seen.add(message);
13488
+ logger?.warn(message);
13489
+ };
13490
+ }
13491
+ /**
13368
13492
  * Convert canonical hooks config to tool-specific format (shared by Claude and Factory Droid).
13369
13493
  * Uses explicit event name mapping tables rather than algorithmic case conversion,
13370
13494
  * since tool event names may differ entirely from canonical names
@@ -13376,6 +13500,7 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
13376
13500
  toolOverrideHooks,
13377
13501
  supportedEvents: converterConfig.supportedEvents
13378
13502
  });
13503
+ const warn = warnOnce(logger);
13379
13504
  const result = {};
13380
13505
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
13381
13506
  const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
@@ -13389,7 +13514,9 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
13389
13514
  if (isNoMatcherEvent && matcherKey) logger?.warn(`matcher "${matcherKey}" on "${eventName}" hook will be ignored — this event does not support matchers`);
13390
13515
  const hooks = buildToolHooks({
13391
13516
  defs,
13392
- converterConfig
13517
+ eventName,
13518
+ converterConfig,
13519
+ warn
13393
13520
  });
13394
13521
  if (hooks.length === 0) continue;
13395
13522
  const includeMatcher = matcherKey && !isNoMatcherEvent;
@@ -13495,85 +13622,283 @@ function isSafeStringArray(value) {
13495
13622
  return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
13496
13623
  }
13497
13624
  /**
13625
+ * A raw string kept only if the canonical field it would land in accepts it,
13626
+ * warning when it does not. The canonical string fields are guarded by
13627
+ * `safeString`, so an existing tool config carrying a control character would
13628
+ * otherwise be imported into a file the next generate refuses to read.
13629
+ */
13630
+ function importCanonicalString({ value, canonical, warn }) {
13631
+ if (typeof value !== "string") return;
13632
+ if (satisfiesCanonicalField({
13633
+ value,
13634
+ canonical
13635
+ })) return value;
13636
+ warn?.(describeInvalidScalar({
13637
+ tool: canonical,
13638
+ canonical,
13639
+ value
13640
+ }));
13641
+ }
13642
+ /**
13498
13643
  * Import the payload fields specific to a hook type, type-checking each raw
13499
13644
  * value before it enters the canonical definition.
13500
13645
  */
13501
- function importTypePayloadFields({ h, hookType }) {
13502
- if (hookType === "http") return {
13503
- ...typeof h.url === "string" && { url: h.url },
13504
- ...isStringRecord(h.headers) && { headers: h.headers },
13505
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
13506
- };
13507
- if (hookType === "mcp_tool") return {
13508
- ...typeof h.server === "string" && { server: h.server },
13509
- ...typeof h.tool === "string" && { tool: h.tool },
13510
- ...h.input !== null && typeof h.input === "object" && !Array.isArray(h.input) && { input: h.input }
13511
- };
13512
- if (hookType === "prompt" || hookType === "agent") return typeof h.model === "string" ? { model: h.model } : {};
13646
+ function importTypePayloadFields({ h, hookType, warn }) {
13647
+ if (hookType === "http") {
13648
+ const url = importCanonicalString({
13649
+ value: h.url,
13650
+ canonical: "url",
13651
+ warn
13652
+ });
13653
+ const headers = isStringRecord(h.headers) && !satisfiesCanonicalField({
13654
+ value: h.headers,
13655
+ canonical: "headers"
13656
+ }) ? (warn?.(describeInvalidScalar({
13657
+ tool: "headers",
13658
+ canonical: "headers",
13659
+ value: h.headers
13660
+ })), void 0) : h.headers;
13661
+ return {
13662
+ ...url !== void 0 && { url },
13663
+ ...isStringRecord(headers) && { headers },
13664
+ ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
13665
+ };
13666
+ }
13667
+ if (hookType === "mcp_tool") {
13668
+ const server = importCanonicalString({
13669
+ value: h.server,
13670
+ canonical: "server",
13671
+ warn
13672
+ });
13673
+ const tool = importCanonicalString({
13674
+ value: h.tool,
13675
+ canonical: "tool",
13676
+ warn
13677
+ });
13678
+ return {
13679
+ ...server !== void 0 && { server },
13680
+ ...tool !== void 0 && { tool },
13681
+ ...h.input !== null && typeof h.input === "object" && !Array.isArray(h.input) && { input: h.input }
13682
+ };
13683
+ }
13684
+ if (hookType === "prompt" || hookType === "agent") {
13685
+ const model = importCanonicalString({
13686
+ value: h.model,
13687
+ canonical: "model",
13688
+ warn
13689
+ });
13690
+ return model !== void 0 ? { model } : {};
13691
+ }
13513
13692
  return {};
13514
13693
  }
13515
13694
  /**
13516
- * Convert a single tool hook record into a canonical hook definition.
13695
+ * Import every per-hook passthrough kind for one tool hook record, reversing
13696
+ * the emit side in {@link buildToolHooks}.
13517
13697
  */
13518
- function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
13519
- const command = stripCommandPrefix({
13520
- command: h.command,
13521
- converterConfig
13522
- });
13523
- const hookType = isImportedHookType(h.type) ? h.type : "command";
13524
- const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
13525
- const prompt = typeof h.prompt === "string" ? h.prompt : void 0;
13698
+ function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
13526
13699
  return {
13527
- type: hookType,
13528
- ...command !== void 0 && command !== null && { command },
13529
- ...timeout !== void 0 && timeout !== null && { timeout },
13530
- ...prompt !== void 0 && prompt !== null && { prompt },
13531
- ...importTypePayloadFields({
13700
+ ...importPassthroughFields({
13532
13701
  h,
13533
- hookType
13702
+ hookType,
13703
+ fields: converterConfig.booleanPassthroughFields ?? [],
13704
+ isValid: isBooleanValue,
13705
+ warn
13534
13706
  }),
13535
- ...converterConfig.passthroughFields?.includes("name") && typeof h.name === "string" && { name: h.name },
13536
- ...converterConfig.passthroughFields?.includes("description") && typeof h.description === "string" && { description: h.description },
13537
- ...importBooleanPassthroughFields({
13707
+ ...importPassthroughFields({
13538
13708
  h,
13539
- converterConfig
13709
+ hookType,
13710
+ fields: converterConfig.numberPassthroughFields ?? [],
13711
+ isValid: isImportableNumber,
13712
+ describeInvalid: describeInvalidScalar,
13713
+ warn
13540
13714
  }),
13541
- ...importNumberPassthroughFields({
13715
+ ...importPassthroughFields({
13542
13716
  h,
13543
- converterConfig
13717
+ hookType,
13718
+ fields: converterConfig.stringPassthroughFields ?? [],
13719
+ isValid: isImportableString,
13720
+ describeInvalid: describeInvalidScalar,
13721
+ warn
13544
13722
  }),
13545
- ...importStringPassthroughFields({
13723
+ ...importPassthroughFields({
13546
13724
  h,
13725
+ hookType,
13726
+ fields: converterConfig.arrayPassthroughFields ?? [],
13727
+ isValid: isImportableArray,
13728
+ describeInvalid: describeInvalidArray,
13729
+ warn
13730
+ }),
13731
+ ...importPassthroughFields({
13732
+ h,
13733
+ hookType,
13734
+ fields: converterConfig.recordPassthroughFields ?? [],
13735
+ isValid: isEmittableRecord,
13736
+ describeInvalid: describeInvalidRecord,
13737
+ warn
13738
+ })
13739
+ };
13740
+ }
13741
+ /**
13742
+ * Convert a single tool hook record into a canonical hook definition.
13743
+ */
13744
+ function toolHookToCanonical({ h, rawEntry, converterConfig, warn }) {
13745
+ const hookType = isImportedHookType(h.type) ? h.type : "command";
13746
+ const command = importCanonicalString({
13747
+ value: stripCommandPrefix({
13748
+ command: h.command,
13547
13749
  converterConfig
13548
13750
  }),
13549
- ...importArrayPassthroughFields({
13751
+ canonical: "command",
13752
+ warn
13753
+ });
13754
+ const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
13755
+ const prompt = importCanonicalString({
13756
+ value: h.prompt,
13757
+ canonical: "prompt",
13758
+ warn
13759
+ });
13760
+ const name = importCanonicalString({
13761
+ value: h.name,
13762
+ canonical: "name",
13763
+ warn
13764
+ });
13765
+ const description = importCanonicalString({
13766
+ value: h.description,
13767
+ canonical: "description",
13768
+ warn
13769
+ });
13770
+ const matcher = rawEntry.matcher;
13771
+ return {
13772
+ type: hookType,
13773
+ ...command !== void 0 && command !== null && { command },
13774
+ ...timeout !== void 0 && timeout !== null && { timeout },
13775
+ ...prompt !== void 0 && prompt !== null && { prompt },
13776
+ ...importTypePayloadFields({
13550
13777
  h,
13551
- converterConfig,
13552
- logger
13778
+ hookType,
13779
+ warn
13553
13780
  }),
13554
- ...importRecordPassthroughFields({
13781
+ ...converterConfig.passthroughFields?.includes("name") && name !== void 0 && { name },
13782
+ ...converterConfig.passthroughFields?.includes("description") && description !== void 0 && { description },
13783
+ ...importAllPassthroughFields({
13555
13784
  h,
13556
13785
  hookType,
13557
13786
  converterConfig,
13558
- logger
13787
+ warn
13559
13788
  }),
13560
13789
  ...importGroupPassthroughFields({
13561
13790
  rawEntry,
13562
13791
  converterConfig
13563
13792
  }),
13564
- ...rawEntry.matcher !== void 0 && rawEntry.matcher !== null && rawEntry.matcher !== "" && { matcher: rawEntry.matcher }
13793
+ ...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
13565
13794
  };
13566
13795
  }
13567
13796
  /**
13568
- * Convert a single tool matcher entry into canonical hook definitions.
13797
+ * The fields whose value decides what a hook *is*, listed per hook type. When
13798
+ * one of them cannot be imported, dropping just the field would leave
13799
+ * something worse than nothing: a hook that loses its body runs nothing, and
13800
+ * one that loses its `matcher` fires on *everything* — a silent widening of
13801
+ * what the imported rule does. So the whole definition is skipped instead,
13802
+ * with the reason named. A field that does not define *this* type (a `prompt`
13803
+ * left on a command hook) is not one of them: it is dropped on its own, the
13804
+ * way any other unusable field is.
13569
13805
  */
13570
- function toolMatcherEntryToCanonical({ rawEntry, converterConfig, logger }) {
13571
- return (rawEntry.hooks ?? []).map((h) => toolHookToCanonical({
13806
+ function definingFields({ h, rawEntry, hookType, converterConfig }) {
13807
+ const fields = [{
13808
+ field: "matcher",
13809
+ value: rawEntry.matcher
13810
+ }];
13811
+ if (hookType === "command") fields.push({
13812
+ field: "command",
13813
+ value: typeof h.command === "string" ? stripCommandPrefix({
13814
+ command: h.command,
13815
+ converterConfig
13816
+ }) : h.command
13817
+ });
13818
+ if (hookType === "prompt" || hookType === "agent") fields.push({
13819
+ field: "prompt",
13820
+ value: h.prompt
13821
+ });
13822
+ if (hookType === "http") fields.push({
13823
+ field: "url",
13824
+ value: h.url
13825
+ });
13826
+ if (hookType === "mcp_tool") fields.push({
13827
+ field: "server",
13828
+ value: h.server
13829
+ }, {
13830
+ field: "tool",
13831
+ value: h.tool
13832
+ });
13833
+ return fields;
13834
+ }
13835
+ /**
13836
+ * Why no hook of this matcher group can be imported, or `undefined` when they
13837
+ * can. A group field that *restricts* when its hooks run (`subdividesGroup`)
13838
+ * is the group-level twin of `matcher`: importing the group without it would
13839
+ * widen every hook in it, so the group is skipped instead.
13840
+ */
13841
+ function describeGroupSkipReason({ rawEntry, converterConfig }) {
13842
+ const entry = rawEntry;
13843
+ for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
13844
+ const value = entry[tool];
13845
+ if (subdividesGroup !== true || value === void 0) continue;
13846
+ 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.`;
13847
+ }
13848
+ }
13849
+ /**
13850
+ * Why this hook cannot be imported, or `undefined` when it can.
13851
+ */
13852
+ function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
13853
+ for (const { field, value } of definingFields({
13572
13854
  h,
13573
13855
  rawEntry,
13574
- converterConfig,
13575
- logger
13576
- }));
13856
+ hookType,
13857
+ converterConfig
13858
+ })) {
13859
+ if (value === void 0 || satisfiesCanonicalField({
13860
+ value,
13861
+ canonical: field
13862
+ })) continue;
13863
+ return `Skipping a hook while importing: its "${field}" (${JSON.stringify(value)}) is unusable — ${describeScalarConstraint({
13864
+ canonical: field,
13865
+ value
13866
+ })} Keeping the hook without it would change what it does, so the whole hook is skipped.`;
13867
+ }
13868
+ }
13869
+ /**
13870
+ * Convert a single tool matcher entry into canonical hook definitions.
13871
+ */
13872
+ function toolMatcherEntryToCanonical({ rawEntry, converterConfig, warn }) {
13873
+ const hookDefs = rawEntry.hooks ?? [];
13874
+ const groupSkipReason = describeGroupSkipReason({
13875
+ rawEntry,
13876
+ converterConfig
13877
+ });
13878
+ if (groupSkipReason !== void 0) {
13879
+ warn?.(groupSkipReason);
13880
+ return [];
13881
+ }
13882
+ const definitions = [];
13883
+ for (const h of hookDefs) {
13884
+ const skipReason = describeHookSkipReason({
13885
+ h,
13886
+ rawEntry,
13887
+ hookType: isImportedHookType(h.type) ? h.type : "command",
13888
+ converterConfig
13889
+ });
13890
+ if (skipReason !== void 0) {
13891
+ warn?.(skipReason);
13892
+ continue;
13893
+ }
13894
+ definitions.push(toolHookToCanonical({
13895
+ h,
13896
+ rawEntry,
13897
+ converterConfig,
13898
+ warn
13899
+ }));
13900
+ }
13901
+ return definitions;
13577
13902
  }
13578
13903
  /**
13579
13904
  * Assemble the canonical hooks config a tool importer writes to
@@ -13603,6 +13928,7 @@ function buildImportedHooksConfig({ hooks, overrideKey, version = 1, extraOverri
13603
13928
  }
13604
13929
  function toolHooksToCanonical({ hooks, converterConfig, logger }) {
13605
13930
  if (hooks === null || hooks === void 0 || typeof hooks !== "object") return {};
13931
+ const warn = warnOnce(logger);
13606
13932
  const canonical = {};
13607
13933
  for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
13608
13934
  const eventName = converterConfig.toolToCanonicalEventNames[toolEventName] ?? toolEventName;
@@ -13613,7 +13939,7 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
13613
13939
  defs.push(...toolMatcherEntryToCanonical({
13614
13940
  rawEntry,
13615
13941
  converterConfig,
13616
- logger
13942
+ warn
13617
13943
  }));
13618
13944
  }
13619
13945
  if (defs.length > 0) canonical[eventName] = defs;
@@ -13652,7 +13978,9 @@ const ANTIGRAVITY_HOOK_NAME = "rulesync";
13652
13978
  * map for import. Accepts both the documented named-hook shape
13653
13979
  * (`{ "<name>": { "<Event>": [...], "enabled"?: bool } }`) and a legacy flat
13654
13980
  * shape (`{ "<Event>": [...] }`) so older or hand-written files still import.
13655
- * The per-hook `enabled` flag is ignored (canonical hooks have no equivalent).
13981
+ * The per-hook `enabled` flag is ignored. The canonical `enabled` field is a
13982
+ * property of a single hook definition, whereas Antigravity's flag gates a whole
13983
+ * named group, so the two do not map onto each other.
13656
13984
  */
13657
13985
  function flattenAntigravityHooks(parsed) {
13658
13986
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -13734,7 +14062,7 @@ var AntigravityHooks = class extends ToolHooks {
13734
14062
  validate
13735
14063
  });
13736
14064
  }
13737
- toRulesyncHooks() {
14065
+ toRulesyncHooks({ logger } = {}) {
13738
14066
  let parsed;
13739
14067
  try {
13740
14068
  parsed = JSON.parse(this.getFileContent());
@@ -13743,7 +14071,8 @@ var AntigravityHooks = class extends ToolHooks {
13743
14071
  }
13744
14072
  const hooks = toolHooksToCanonical({
13745
14073
  hooks: flattenAntigravityHooks(parsed),
13746
- converterConfig: ANTIGRAVITY_CONVERTER_CONFIG
14074
+ converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
14075
+ logger
13747
14076
  });
13748
14077
  const overrideKey = this.constructor.getOverrideKey();
13749
14078
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
@@ -14125,7 +14454,7 @@ var ClaudecodeHooks = class extends ToolHooks {
14125
14454
  validate
14126
14455
  });
14127
14456
  }
14128
- toRulesyncHooks() {
14457
+ toRulesyncHooks({ logger } = {}) {
14129
14458
  let settings;
14130
14459
  try {
14131
14460
  settings = JSON.parse(this.getFileContent());
@@ -14134,7 +14463,8 @@ var ClaudecodeHooks = class extends ToolHooks {
14134
14463
  }
14135
14464
  const hooks = toolHooksToCanonical({
14136
14465
  hooks: settings.hooks,
14137
- converterConfig: this.constructor.getConverterConfig()
14466
+ converterConfig: this.constructor.getConverterConfig(),
14467
+ logger
14138
14468
  });
14139
14469
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
14140
14470
  hooks,
@@ -14293,13 +14623,14 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
14293
14623
  validate
14294
14624
  });
14295
14625
  }
14296
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
14626
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
14297
14627
  const paths = CodexcliHooks.getSettablePaths({ global });
14298
14628
  const config = rulesyncHooks.getJson();
14299
14629
  const codexHooks = canonicalToToolHooks({
14300
14630
  config,
14301
14631
  toolOverrideHooks: config.codexcli?.hooks,
14302
- converterConfig: CODEXCLI_CONVERTER_CONFIG
14632
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
14633
+ logger
14303
14634
  });
14304
14635
  const fileContent = JSON.stringify({ hooks: codexHooks }, null, 2);
14305
14636
  return new CodexcliHooks({
@@ -14310,7 +14641,7 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
14310
14641
  validate
14311
14642
  });
14312
14643
  }
14313
- toRulesyncHooks() {
14644
+ toRulesyncHooks({ logger } = {}) {
14314
14645
  let parsed;
14315
14646
  try {
14316
14647
  parsed = JSON.parse(this.getFileContent());
@@ -14319,7 +14650,8 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
14319
14650
  }
14320
14651
  const hooks = toolHooksToCanonical({
14321
14652
  hooks: parsed.hooks,
14322
- converterConfig: CODEXCLI_CONVERTER_CONFIG
14653
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
14654
+ logger
14323
14655
  });
14324
14656
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
14325
14657
  hooks,
@@ -14558,13 +14890,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
14558
14890
  * under `.github/hooks/` is picked up automatically when the CLI is
14559
14891
  * invoked from the project root.
14560
14892
  *
14561
- * - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` — chosen for
14562
- * consistency with the existing global Copilot CLI config layout (e.g.
14563
- * `~/.copilot/mcp-config.json` produced by `copilotcli-mcp.ts`). The
14564
- * official docs do not currently document a global hooks location, so
14565
- * this is a rulesync convention pending official documentation; we keep
14566
- * all rulesync-managed Copilot CLI files under the single `~/.copilot/`
14567
- * root and will revisit if the spec later mandates an alternate layout.
14893
+ * - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` — the directory is
14894
+ * the documented user-level hooks location ("`*.json` files in the
14895
+ * user-level hooks directory. By default this is `~/.copilot/hooks/` on
14896
+ * macOS and Linux, or `%USERPROFILE%\.copilot\hooks\` on Windows"). Every
14897
+ * `*.json` in it is loaded, so the filename remains rulesync's choice, as it
14898
+ * is for project scope. `COPILOT_HOME` relocates the directory upstream
14899
+ * (`$COPILOT_HOME/hooks/`); rulesync does not read that variable yet.
14568
14900
  *
14569
14901
  * Hook entries on the six matcher-aware events (see
14570
14902
  * {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
@@ -14975,17 +15307,18 @@ const DEEPAGENTS_MCP_FILE_NAME = ".mcp.json";
14975
15307
  const DEEPAGENTS_HOOKS_FILE_NAME = "hooks.json";
14976
15308
  //#endregion
14977
15309
  //#region src/features/hooks/deepagents-hooks.ts
14978
- function isDeepagentsHooksFile(val) {
14979
- if (typeof val !== "object" || val === null || !("hooks" in val)) return false;
14980
- return Array.isArray(val.hooks);
15310
+ function isRecord(value) {
15311
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14981
15312
  }
14982
15313
  /**
14983
- * Convert canonical hooks config to deepagents flat array format.
15314
+ * Convert the canonical hooks config to the deepagents Hooks v2 document.
14984
15315
  *
14985
- * deepagents format:
14986
- * { "hooks": [{ "command": ["bash", "-c", "..."], "events": ["session.start"] }] }
15316
+ * ```json
15317
+ * { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "" }] }] } }
15318
+ * ```
14987
15319
  *
14988
- * Each canonical hook definition becomes one deepagents hook entry.
15320
+ * Definitions sharing an event and matcher land in one group, preserving their
15321
+ * authored order — upstream runs a group's handlers in sequence.
14989
15322
  */
14990
15323
  function canonicalToDeepagentsHooks(config) {
14991
15324
  const supported = new Set(DEEPAGENTS_HOOK_EVENTS);
@@ -14993,7 +15326,7 @@ function canonicalToDeepagentsHooks(config) {
14993
15326
  ...config.hooks,
14994
15327
  ...config.deepagents?.hooks
14995
15328
  };
14996
- const entries = [];
15329
+ const hooks = {};
14997
15330
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
14998
15331
  if (!supported.has(canonicalEvent)) continue;
14999
15332
  const deepagentsEvent = CANONICAL_TO_DEEPAGENTS_EVENT_NAMES[canonicalEvent];
@@ -15001,43 +15334,69 @@ function canonicalToDeepagentsHooks(config) {
15001
15334
  for (const def of definitions) {
15002
15335
  if ((def.type ?? "command") !== "command") continue;
15003
15336
  if (!def.command) continue;
15004
- if (def.matcher) continue;
15005
- entries.push({
15006
- command: [
15007
- "bash",
15008
- "-c",
15009
- def.command
15010
- ],
15011
- events: [deepagentsEvent]
15337
+ const handler = {
15338
+ type: "command",
15339
+ command: def.command
15340
+ };
15341
+ if (def.timeout !== void 0 && def.timeout !== null && def.timeout > 0) handler.timeout = def.timeout;
15342
+ if (def.statusMessage !== void 0 && def.statusMessage !== null) handler.statusMessage = def.statusMessage;
15343
+ const matcher = def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" ? def.matcher : void 0;
15344
+ const groups = hooks[deepagentsEvent] ??= [];
15345
+ const group = groups.find((candidate) => candidate.matcher === matcher);
15346
+ if (group) group.hooks.push(handler);
15347
+ else groups.push({
15348
+ ...matcher !== void 0 && { matcher },
15349
+ hooks: [handler]
15012
15350
  });
15013
15351
  }
15014
15352
  }
15015
- return entries;
15353
+ return hooks;
15016
15354
  }
15017
15355
  /**
15018
- * Convert deepagents flat array format back to canonical hooks record.
15356
+ * Convert the Hooks v2 document back to the canonical hooks record.
15019
15357
  */
15020
- function deepagentsToCanonicalHooks(hooksEntries) {
15358
+ function deepagentsToCanonicalHooks(hooks) {
15021
15359
  const canonical = {};
15022
- for (const entry of hooksEntries) {
15023
- if (typeof entry !== "object" || entry === null) continue;
15024
- if (!Array.isArray(entry.command) || entry.command.length === 0) continue;
15025
- let command;
15026
- if (entry.command.length === 3 && entry.command[0] === "bash" && entry.command[1] === "-c") command = entry.command[2] ?? "";
15027
- else command = entry.command.join(" ");
15028
- const events = entry.events ?? [];
15029
- for (const deepagentsEvent of events) {
15030
- const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
15360
+ for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
15361
+ const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
15362
+ if (!canonicalEvent || !Array.isArray(groups)) continue;
15363
+ for (const group of groups) {
15364
+ if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
15365
+ for (const handler of group.hooks) {
15366
+ if (!isRecord(handler) || typeof handler.command !== "string") continue;
15367
+ const def = {
15368
+ type: "command",
15369
+ command: handler.command
15370
+ };
15371
+ if (typeof group.matcher === "string" && group.matcher !== "") def.matcher = group.matcher;
15372
+ if (typeof handler.timeout === "number") def.timeout = handler.timeout;
15373
+ if (typeof handler.statusMessage === "string") def.statusMessage = handler.statusMessage;
15374
+ (canonical[canonicalEvent] ??= []).push(def);
15375
+ }
15376
+ }
15377
+ }
15378
+ return canonical;
15379
+ }
15380
+ /**
15381
+ * Read the pre-v2 flat list. deepagents still loads it until 2026-09-01, so a
15382
+ * `hooks.json` a user has not migrated yet is imported rather than discarded —
15383
+ * but rulesync only ever writes the v2 shape, so regenerating migrates it.
15384
+ */
15385
+ function deepagentsLegacyToCanonicalHooks(entries) {
15386
+ const canonical = {};
15387
+ for (const entry of entries) {
15388
+ if (!isRecord(entry)) continue;
15389
+ const argv = entry.command;
15390
+ if (!Array.isArray(argv) || argv.length === 0) continue;
15391
+ const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
15392
+ const events = Array.isArray(entry.events) ? entry.events : [];
15393
+ for (const legacyEvent of events) {
15394
+ const canonicalEvent = typeof legacyEvent === "string" ? DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES[legacyEvent] : void 0;
15031
15395
  if (!canonicalEvent) continue;
15032
- const existing = canonical[canonicalEvent];
15033
- if (existing) existing.push({
15396
+ (canonical[canonicalEvent] ??= []).push({
15034
15397
  type: "command",
15035
15398
  command
15036
15399
  });
15037
- else canonical[canonicalEvent] = [{
15038
- type: "command",
15039
- command
15040
- }];
15041
15400
  }
15042
15401
  }
15043
15402
  return canonical;
@@ -15046,7 +15405,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15046
15405
  constructor(params) {
15047
15406
  super({
15048
15407
  ...params,
15049
- fileContent: params.fileContent ?? JSON.stringify({ hooks: [] }, null, 2)
15408
+ fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
15050
15409
  });
15051
15410
  }
15052
15411
  isDeletable() {
@@ -15060,7 +15419,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15060
15419
  }
15061
15420
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
15062
15421
  const paths = DeepagentsHooks.getSettablePaths({ global });
15063
- const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: [] }, null, 2);
15422
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} }, null, 2);
15064
15423
  return new DeepagentsHooks({
15065
15424
  outputRoot,
15066
15425
  relativeDirPath: paths.relativeDirPath,
@@ -15088,7 +15447,8 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15088
15447
  } catch (error) {
15089
15448
  throw new Error(`Failed to parse deepagents hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
15090
15449
  }
15091
- const hooks = deepagentsToCanonicalHooks(isDeepagentsHooksFile(parsed) ? parsed.hooks : []);
15450
+ const rawHooks = isRecord(parsed) ? parsed.hooks : void 0;
15451
+ const hooks = Array.isArray(rawHooks) ? deepagentsLegacyToCanonicalHooks(rawHooks) : isRecord(rawHooks) ? deepagentsToCanonicalHooks(rawHooks) : {};
15092
15452
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15093
15453
  hooks,
15094
15454
  overrideKey: "deepagents"
@@ -15105,7 +15465,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
15105
15465
  outputRoot,
15106
15466
  relativeDirPath,
15107
15467
  relativeFilePath,
15108
- fileContent: JSON.stringify({ hooks: [] }, null, 2),
15468
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
15109
15469
  validate: false
15110
15470
  });
15111
15471
  }
@@ -15207,7 +15567,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
15207
15567
  validate
15208
15568
  });
15209
15569
  }
15210
- toRulesyncHooks() {
15570
+ toRulesyncHooks({ logger } = {}) {
15211
15571
  let parsed;
15212
15572
  try {
15213
15573
  parsed = JSON.parse(this.getFileContent());
@@ -15215,8 +15575,9 @@ var DevinHooks = class DevinHooks extends ToolHooks {
15215
15575
  throw new Error(`Failed to parse Devin hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
15216
15576
  }
15217
15577
  const hooks = toolHooksToCanonical({
15218
- hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
15219
- converterConfig: DEVIN_CONVERTER_CONFIG
15578
+ hooks: this.getRelativeFilePath() === "config.json" ? isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {} : parsed,
15579
+ converterConfig: DEVIN_CONVERTER_CONFIG,
15580
+ logger
15220
15581
  });
15221
15582
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15222
15583
  hooks,
@@ -15310,7 +15671,7 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
15310
15671
  validate
15311
15672
  });
15312
15673
  }
15313
- toRulesyncHooks() {
15674
+ toRulesyncHooks({ logger } = {}) {
15314
15675
  let settings;
15315
15676
  try {
15316
15677
  settings = JSON.parse(this.getFileContent());
@@ -15319,7 +15680,8 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
15319
15680
  }
15320
15681
  const hooks = toolHooksToCanonical({
15321
15682
  hooks: settings.hooks,
15322
- converterConfig: FACTORYDROID_CONVERTER_CONFIG
15683
+ converterConfig: FACTORYDROID_CONVERTER_CONFIG,
15684
+ logger
15323
15685
  });
15324
15686
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15325
15687
  hooks,
@@ -15388,13 +15750,14 @@ var GooseHooks = class GooseHooks extends ToolHooks {
15388
15750
  validate
15389
15751
  });
15390
15752
  }
15391
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
15753
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
15392
15754
  const paths = GooseHooks.getSettablePaths({ global });
15393
15755
  const config = rulesyncHooks.getJson();
15394
15756
  const gooseHooks = canonicalToToolHooks({
15395
15757
  config,
15396
15758
  toolOverrideHooks: config.goose?.hooks,
15397
- converterConfig: GOOSE_CONVERTER_CONFIG
15759
+ converterConfig: GOOSE_CONVERTER_CONFIG,
15760
+ logger
15398
15761
  });
15399
15762
  const fileContent = JSON.stringify({ hooks: gooseHooks }, null, 2);
15400
15763
  return new GooseHooks({
@@ -15405,7 +15768,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
15405
15768
  validate
15406
15769
  });
15407
15770
  }
15408
- toRulesyncHooks() {
15771
+ toRulesyncHooks({ logger } = {}) {
15409
15772
  let parsed;
15410
15773
  try {
15411
15774
  parsed = JSON.parse(this.getFileContent());
@@ -15414,7 +15777,8 @@ var GooseHooks = class GooseHooks extends ToolHooks {
15414
15777
  }
15415
15778
  const hooks = toolHooksToCanonical({
15416
15779
  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,
15417
- converterConfig: GOOSE_CONVERTER_CONFIG
15780
+ converterConfig: GOOSE_CONVERTER_CONFIG,
15781
+ logger
15418
15782
  });
15419
15783
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15420
15784
  hooks,
@@ -15518,7 +15882,7 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
15518
15882
  validate
15519
15883
  });
15520
15884
  }
15521
- toRulesyncHooks() {
15885
+ toRulesyncHooks({ logger } = {}) {
15522
15886
  let parsed;
15523
15887
  try {
15524
15888
  parsed = JSON.parse(this.getFileContent());
@@ -15526,8 +15890,9 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
15526
15890
  throw new Error(`Failed to parse Grok hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
15527
15891
  }
15528
15892
  const hooks = toolHooksToCanonical({
15529
- hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
15530
- converterConfig: GROKCLI_CONVERTER_CONFIG
15893
+ hooks: isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {},
15894
+ converterConfig: GROKCLI_CONVERTER_CONFIG,
15895
+ logger
15531
15896
  });
15532
15897
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15533
15898
  hooks,
@@ -15848,7 +16213,7 @@ var JunieHooks = class JunieHooks extends ToolHooks {
15848
16213
  validate
15849
16214
  });
15850
16215
  }
15851
- toRulesyncHooks() {
16216
+ toRulesyncHooks({ logger } = {}) {
15852
16217
  let settings;
15853
16218
  try {
15854
16219
  settings = JSON.parse(this.getFileContent());
@@ -15857,7 +16222,8 @@ var JunieHooks = class JunieHooks extends ToolHooks {
15857
16222
  }
15858
16223
  const hooks = toolHooksToCanonical({
15859
16224
  hooks: settings.hooks,
15860
- converterConfig: JUNIE_CONVERTER_CONFIG
16225
+ converterConfig: JUNIE_CONVERTER_CONFIG,
16226
+ logger
15861
16227
  });
15862
16228
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
15863
16229
  hooks,
@@ -16595,7 +16961,7 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
16595
16961
  ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
16596
16962
  action,
16597
16963
  ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
16598
- enabled: true
16964
+ enabled: def.enabled ?? true
16599
16965
  });
16600
16966
  }
16601
16967
  return entries;
@@ -16635,6 +17001,7 @@ function kiroIdeHooksToCanonical(entries) {
16635
17001
  if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
16636
17002
  if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
16637
17003
  if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
17004
+ if (entry.enabled === false) def.enabled = false;
16638
17005
  (canonical[eventName] ??= []).push(def);
16639
17006
  }
16640
17007
  return canonical;
@@ -17579,6 +17946,37 @@ function unsupportedEventNames(params) {
17579
17946
  const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
17580
17947
  return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
17581
17948
  }
17949
+ /**
17950
+ * A logger whose warnings name the tool being converted, matching the rest of
17951
+ * this processor's warnings: the shared hooks converter says what is wrong but
17952
+ * not which tool's file it is reading or writing, and one run walks them all.
17953
+ *
17954
+ * Every method delegates explicitly rather than through a prototype, so the
17955
+ * real logger keeps owning its state — a wrapper that inherited it would
17956
+ * absorb the writes `configure` and `outputJson` make.
17957
+ */
17958
+ function withToolTargetPrefix({ logger, toolTarget }) {
17959
+ return {
17960
+ configure: (options) => logger.configure(options),
17961
+ get verbose() {
17962
+ return logger.verbose;
17963
+ },
17964
+ get silent() {
17965
+ return logger.silent;
17966
+ },
17967
+ get jsonMode() {
17968
+ return logger.jsonMode;
17969
+ },
17970
+ captureData: (key, value) => logger.captureData(key, value),
17971
+ getJsonData: () => logger.getJsonData(),
17972
+ outputJson: (success, error) => logger.outputJson(success, error),
17973
+ info: (message, ...args) => logger.info(message, ...args),
17974
+ success: (message, ...args) => logger.success(message, ...args),
17975
+ warn: (message, ...args) => logger.warn(`For ${toolTarget}: ${message}`, ...args),
17976
+ error: (message, code, ...args) => logger.error(message, code, ...args),
17977
+ debug: (message, ...args) => logger.debug(message, ...args)
17978
+ };
17979
+ }
17582
17980
  function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
17583
17981
  if (factory.supportsMatcher && !factory.matcherEvents) return [];
17584
17982
  const matcherEvents = factory.matcherEvents ? new Set(factory.matcherEvents) : void 0;
@@ -17801,13 +18199,13 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17801
18199
  ["deepagents", {
17802
18200
  class: DeepagentsHooks,
17803
18201
  meta: {
17804
- supportsProject: false,
18202
+ supportsProject: true,
17805
18203
  supportsGlobal: true,
17806
18204
  supportsImport: true
17807
18205
  },
17808
18206
  supportedEvents: DEEPAGENTS_HOOK_EVENTS,
17809
18207
  supportedHookTypes: ["command"],
17810
- supportsMatcher: false
18208
+ supportsMatcher: true
17811
18209
  }],
17812
18210
  ["kiro", {
17813
18211
  class: KiroHooks,
@@ -18019,6 +18417,15 @@ var HooksProcessor = class extends FeatureProcessor {
18019
18417
  }
18020
18418
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
18021
18419
  }
18420
+ if (this.toolTarget !== "kiro-ide") {
18421
+ const skippedEvents = new Set(unsupportedEventNames({
18422
+ factory,
18423
+ sharedHooks,
18424
+ effectiveHooks
18425
+ }));
18426
+ const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
18427
+ 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(", ")}`);
18428
+ }
18022
18429
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
18023
18430
  factory,
18024
18431
  effectiveHooks
@@ -18029,7 +18436,10 @@ var HooksProcessor = class extends FeatureProcessor {
18029
18436
  rulesyncHooks,
18030
18437
  validate: true,
18031
18438
  global: this.global,
18032
- logger: this.logger
18439
+ logger: withToolTargetPrefix({
18440
+ logger: this.logger,
18441
+ toolTarget: this.toolTarget
18442
+ })
18033
18443
  })];
18034
18444
  const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
18035
18445
  outputRoot: this.outputRoot,
@@ -18039,7 +18449,12 @@ var HooksProcessor = class extends FeatureProcessor {
18039
18449
  return result;
18040
18450
  }
18041
18451
  async convertToolFilesToRulesyncFiles(toolFiles) {
18042
- return toolFiles.filter((f) => f instanceof ToolHooks).map((h) => h.toRulesyncHooks({ logger: this.logger }));
18452
+ const hooks = toolFiles.filter((f) => f instanceof ToolHooks);
18453
+ const logger = withToolTargetPrefix({
18454
+ logger: this.logger,
18455
+ toolTarget: this.toolTarget
18456
+ });
18457
+ return hooks.map((h) => h.toRulesyncHooks({ logger }));
18043
18458
  }
18044
18459
  static getToolTargets({ global = false, importOnly = false } = {}) {
18045
18460
  if (global) return importOnly ? hooksProcessorToolTargetsGlobalImportable : hooksProcessorToolTargetsGlobal;
@@ -19682,9 +20097,9 @@ function parseAmpSettingsJsonc(fileContent) {
19682
20097
  }
19683
20098
  function filterMcpServers(mcpServers) {
19684
20099
  const filtered = {};
19685
- if (!isRecord(mcpServers)) return filtered;
20100
+ if (!isRecord$1(mcpServers)) return filtered;
19686
20101
  for (const [name, config] of Object.entries(mcpServers)) {
19687
- if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
20102
+ if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
19688
20103
  const filteredConfig = {};
19689
20104
  for (const [key, value] of Object.entries(config)) {
19690
20105
  if (isPrototypePollutionKey(key)) continue;
@@ -19799,7 +20214,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
19799
20214
  success: true,
19800
20215
  error: null
19801
20216
  };
19802
- if (!isRecord(mcpServers)) return {
20217
+ if (!isRecord$1(mcpServers)) return {
19803
20218
  success: false,
19804
20219
  error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
19805
20220
  };
@@ -19808,7 +20223,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
19808
20223
  success: false,
19809
20224
  error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
19810
20225
  };
19811
- if (!isRecord(serverConfig)) return {
20226
+ if (!isRecord$1(serverConfig)) return {
19812
20227
  success: false,
19813
20228
  error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
19814
20229
  };
@@ -20408,13 +20823,13 @@ function normalizeCodexMcpServerName(name) {
20408
20823
  function convertFromCodexFormat(codexMcp) {
20409
20824
  const result = {};
20410
20825
  for (const [name, config] of Object.entries(codexMcp)) {
20411
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
20826
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
20412
20827
  const converted = {};
20413
20828
  for (const [key, value] of Object.entries(config)) {
20414
20829
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
20415
20830
  if (key === "enabled") {
20416
20831
  if (value === false) converted["disabled"] = true;
20417
- } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
20832
+ } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
20418
20833
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
20419
20834
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
20420
20835
  if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
@@ -20433,7 +20848,7 @@ function convertToCodexFormat(mcpServers) {
20433
20848
  const result = {};
20434
20849
  const originalNames = /* @__PURE__ */ new Map();
20435
20850
  for (const [name, config] of Object.entries(mcpServers)) {
20436
- if (!isRecord(config)) continue;
20851
+ if (!isRecord$1(config)) continue;
20437
20852
  const { codexName, usedFallback } = normalizeCodexMcpServerName(name);
20438
20853
  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.`);
20439
20854
  const converted = {};
@@ -20441,7 +20856,7 @@ function convertToCodexFormat(mcpServers) {
20441
20856
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
20442
20857
  if (key === "disabled") {
20443
20858
  if (value === true) converted["enabled"] = false;
20444
- } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
20859
+ } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
20445
20860
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
20446
20861
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
20447
20862
  if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
@@ -20517,21 +20932,21 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
20517
20932
  const strippedMcpServers = rulesyncMcp.getMcpServers();
20518
20933
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
20519
20934
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
20520
- const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
20935
+ const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
20521
20936
  return [serverName, {
20522
20937
  ...serverConfig,
20523
- ...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
20524
- ...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
20525
- ...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
20938
+ ...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
20939
+ ...isRecord$1(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
20940
+ ...isRecord$1(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
20526
20941
  }];
20527
20942
  })));
20528
20943
  const filteredMcpServers = this.removeEmptyEntries(converted);
20529
20944
  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`);
20530
- const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
20945
+ const existingMcpServers = isRecord$1(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
20531
20946
  const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
20532
- const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
20947
+ const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
20533
20948
  const serverRecord = serverConfig;
20534
- if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
20949
+ if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
20535
20950
  ...serverRecord,
20536
20951
  tools: existingServer["tools"]
20537
20952
  }];
@@ -21488,7 +21903,7 @@ function convertServerToGooseExtension(name, config, logger) {
21488
21903
  function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
21489
21904
  const generated = {};
21490
21905
  for (const [name, config] of Object.entries(mcpServers)) {
21491
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
21906
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
21492
21907
  const ext = convertServerToGooseExtension(name, config, logger);
21493
21908
  if (ext !== void 0) generated[name] = ext;
21494
21909
  }
@@ -21496,7 +21911,7 @@ function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
21496
21911
  const retracted = [];
21497
21912
  for (const [name, ext] of Object.entries(existingExtensions)) {
21498
21913
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
21499
- const type = isRecord(ext) ? existingExtensionType(ext) : void 0;
21914
+ const type = isRecord$1(ext) ? existingExtensionType(ext) : void 0;
21500
21915
  if (type !== void 0 && GOOSE_MCP_EXTENSION_TYPES.has(type)) {
21501
21916
  if (!Object.hasOwn(generated, name)) retracted.push(name);
21502
21917
  continue;
@@ -21530,7 +21945,7 @@ function convertFromGooseFormat(extensions) {
21530
21945
  const result = {};
21531
21946
  const skipped = [];
21532
21947
  for (const [name, ext] of Object.entries(extensions)) {
21533
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
21948
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
21534
21949
  const type = existingExtensionType(ext);
21535
21950
  if (type === void 0 || !GOOSE_MCP_EXTENSION_TYPES.has(type)) {
21536
21951
  skipped.push(name);
@@ -21584,7 +21999,7 @@ function buildGoosePluginStdioServer(config) {
21584
21999
  function convertToGoosePluginMcpServers(mcpServers, logger) {
21585
22000
  const result = {};
21586
22001
  for (const [name, config] of Object.entries(mcpServers)) {
21587
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22002
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
21588
22003
  const gooseType = resolveGooseType(config, resolveGooseUrl(config));
21589
22004
  if (gooseType !== "stdio") {
21590
22005
  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.`);
@@ -21625,7 +22040,7 @@ var GooseMcp = class GooseMcp extends ToolMcp {
21625
22040
  parsePluginManifest(fileContent) {
21626
22041
  try {
21627
22042
  const parsed = JSON.parse(fileContent);
21628
- return isRecord(parsed) ? parsed : {};
22043
+ return isRecord$1(parsed) ? parsed : {};
21629
22044
  } catch (error) {
21630
22045
  throw new Error(`Failed to parse Goose MCP manifest at ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
21631
22046
  }
@@ -21674,7 +22089,7 @@ var GooseMcp = class GooseMcp extends ToolMcp {
21674
22089
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21675
22090
  const existingContent = await readFileContentOrNull(filePath) ?? "";
21676
22091
  const config = parseGooseConfig(existingContent, paths.relativeDirPath, paths.relativeFilePath);
21677
- const existingExtensions = isRecord(config.extensions) ? config.extensions : {};
22092
+ const existingExtensions = isRecord$1(config.extensions) ? config.extensions : {};
21678
22093
  return new GooseMcp({
21679
22094
  outputRoot,
21680
22095
  relativeDirPath: paths.relativeDirPath,
@@ -21696,10 +22111,10 @@ var GooseMcp = class GooseMcp extends ToolMcp {
21696
22111
  }
21697
22112
  toRulesyncMcp() {
21698
22113
  if (!this.global) {
21699
- const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
22114
+ const mcpServers = isRecord$1(this.config.mcpServers) ? this.config.mcpServers : {};
21700
22115
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
21701
22116
  }
21702
- const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
22117
+ const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
21703
22118
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
21704
22119
  }
21705
22120
  validate() {
@@ -21735,7 +22150,7 @@ function convertToGrokFormat(mcpServers) {
21735
22150
  const result = {};
21736
22151
  for (const [name, config] of Object.entries(mcpServers)) {
21737
22152
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
21738
- if (!isRecord(config)) continue;
22153
+ if (!isRecord$1(config)) continue;
21739
22154
  const converted = {};
21740
22155
  for (const [key, value] of Object.entries(config)) {
21741
22156
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -21750,7 +22165,7 @@ function convertToGrokFormat(mcpServers) {
21750
22165
  function convertFromGrokFormat(grokMcp) {
21751
22166
  const result = {};
21752
22167
  for (const [name, config] of Object.entries(grokMcp)) {
21753
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22168
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
21754
22169
  const converted = {};
21755
22170
  for (const [key, value] of Object.entries(config)) {
21756
22171
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -21888,7 +22303,7 @@ function resolveHermesTimeout(config) {
21888
22303
  * import alike. See the Hermes mcp-config-reference.
21889
22304
  */
21890
22305
  function copyHermesOauth(source) {
21891
- if (!isRecord(source)) return;
22306
+ if (!isRecord$1(source)) return;
21892
22307
  const oauth = {};
21893
22308
  for (const key of [
21894
22309
  "redirect_uri",
@@ -22022,13 +22437,13 @@ function convertServerToHermes(config) {
22022
22437
  function convertToHermesFormat(mcpServers) {
22023
22438
  const result = {};
22024
22439
  for (const [name, config] of Object.entries(mcpServers)) {
22025
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
22440
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
22026
22441
  result[name] = convertServerToHermes(config);
22027
22442
  }
22028
22443
  return result;
22029
22444
  }
22030
22445
  function mergeHermesMcpServers(config, mcpServers) {
22031
- const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
22446
+ const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
22032
22447
  return {
22033
22448
  ...config,
22034
22449
  mcp_servers: {
@@ -22047,7 +22462,7 @@ function convertFromHermesFormat(mcpServers) {
22047
22462
  const result = {};
22048
22463
  const hermesOverrides = {};
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
  const server = {};
22052
22467
  if (typeof config.command === "string") server.command = config.command;
22053
22468
  if (isStringArray$1(config.args)) server.args = config.args;
@@ -22056,7 +22471,7 @@ function convertFromHermesFormat(mcpServers) {
22056
22471
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
22057
22472
  if (config.enabled === false) server.disabled = true;
22058
22473
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
22059
- if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
22474
+ if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
22060
22475
  result[name] = server;
22061
22476
  const hermesServer = { ...server };
22062
22477
  if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
@@ -22095,7 +22510,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
22095
22510
  const merged = mergeHermesMcpServers(parseSharedConfig({
22096
22511
  format: "yaml",
22097
22512
  fileContent
22098
- }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
22513
+ }), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
22099
22514
  this.config = merged;
22100
22515
  super.setFileContent(applySharedConfigPatch({
22101
22516
  fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
@@ -22159,7 +22574,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
22159
22574
  });
22160
22575
  }
22161
22576
  toRulesyncMcp() {
22162
- const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
22577
+ const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
22163
22578
  return this.toRulesyncMcpDefault({
22164
22579
  outputRoot: getHermesagentRulesyncOutputRoot({
22165
22580
  nativeOutputRoot: this.outputRoot,
@@ -22471,7 +22886,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
22471
22886
  */
22472
22887
  function readExistingKiloMcpEntries(fileContent) {
22473
22888
  const mcp = parse(fileContent || "{}")?.mcp;
22474
- if (!isRecord(mcp)) return {};
22889
+ if (!isRecord$1(mcp)) return {};
22475
22890
  const entries = {};
22476
22891
  for (const [serverName, entry] of Object.entries(mcp)) {
22477
22892
  const result = KiloMcpServerSchema.safeParse(entry);
@@ -22775,7 +23190,7 @@ async function readKimiCodeConfig({ outputRoot }) {
22775
23190
  return {
22776
23191
  parsed: true,
22777
23192
  content,
22778
- mcp: isRecord(mcp) ? mcp : {}
23193
+ mcp: isRecord$1(mcp) ? mcp : {}
22779
23194
  };
22780
23195
  } catch {
22781
23196
  return {
@@ -22929,7 +23344,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
22929
23344
  static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
22930
23345
  if (!global) return [];
22931
23346
  const block = rulesyncMcp.getJson()["kimi-code"];
22932
- if (!isRecord(block)) return [];
23347
+ if (!isRecord$1(block)) return [];
22933
23348
  const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
22934
23349
  const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
22935
23350
  if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0) return [];
@@ -22982,6 +23397,68 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
22982
23397
  };
22983
23398
  //#endregion
22984
23399
  //#region src/features/mcp/kiro-mcp.ts
23400
+ /**
23401
+ * Union of two optional string lists, preserving order and dropping duplicates.
23402
+ * Returns `undefined` only when neither side was authored at all, so the caller
23403
+ * omits the key entirely rather than writing an empty array — but an explicitly
23404
+ * authored `[]` is kept, which keeps import → generate idempotent.
23405
+ */
23406
+ function mergeToolLists(...lists) {
23407
+ if (lists.every((list) => list === void 0)) return void 0;
23408
+ const merged = [];
23409
+ for (const list of lists) for (const tool of list ?? []) if (!merged.includes(tool)) merged.push(tool);
23410
+ return merged;
23411
+ }
23412
+ /**
23413
+ * Translate rulesync's Kiro-only authoring keys onto the field names Kiro
23414
+ * actually reads in `mcp.json`.
23415
+ *
23416
+ * - `kiroAutoApprove` → `autoApprove` (tools run without a confirmation prompt)
23417
+ * - `kiroAutoBlock` → `disabledTools` (tools hidden from the agent)
23418
+ *
23419
+ * `disabledTools` is the only block list Kiro reads, and it is also a canonical
23420
+ * rulesync field, so `kiroAutoBlock` is a redundant spelling of it. Prefer the
23421
+ * canonical field; see the note on `kiroAutoBlock` in `src/types/mcp.ts`.
23422
+ *
23423
+ * Both native names are documented per-server fields, so a config that already
23424
+ * spells them natively keeps working: the two lists are merged rather than
23425
+ * one overwriting the other.
23426
+ * @see https://kiro.dev/docs/mcp/configuration/
23427
+ */
23428
+ function toKiroMcpServers(servers) {
23429
+ return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
23430
+ const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
23431
+ const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
23432
+ const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
23433
+ return [name, {
23434
+ ...rest,
23435
+ ...autoApprove !== void 0 && { autoApprove },
23436
+ ...disabled !== void 0 && { disabledTools: disabled }
23437
+ }];
23438
+ }));
23439
+ }
23440
+ /**
23441
+ * Import direction of {@link toKiroMcpServers}: Kiro's `autoApprove` becomes the
23442
+ * rulesync-only `kiroAutoApprove` so a regenerate reproduces it. `disabledTools`
23443
+ * is left alone — it is already a canonical rulesync key with the same meaning,
23444
+ * so `kiroAutoBlock` deliberately has no import counterpart.
23445
+ *
23446
+ * Only a genuine string array is renamed. `kiroAutoApprove` is typed as one, so
23447
+ * moving a hand-written `"autoApprove": "all"` there would produce a
23448
+ * `.rulesync/mcp.jsonc` the next generate refuses to parse; such a value stays
23449
+ * under its original key and passes through untouched instead.
23450
+ */
23451
+ function fromKiroMcpServers(servers) {
23452
+ return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
23453
+ if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
23454
+ const { autoApprove, ...rest } = server;
23455
+ if (!isStringArray$1(autoApprove)) return [name, server];
23456
+ return [name, {
23457
+ ...rest,
23458
+ kiroAutoApprove: autoApprove
23459
+ }];
23460
+ }));
23461
+ }
22985
23462
  var KiroMcp = class KiroMcp extends ToolMcp {
22986
23463
  json;
22987
23464
  constructor(params) {
@@ -23010,7 +23487,7 @@ var KiroMcp = class KiroMcp extends ToolMcp {
23010
23487
  }
23011
23488
  static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
23012
23489
  const paths = this.getSettablePaths();
23013
- const fileContent = JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2);
23490
+ const fileContent = JSON.stringify({ mcpServers: toKiroMcpServers(rulesyncMcp.getMcpServers()) }, null, 2);
23014
23491
  return new KiroMcp({
23015
23492
  outputRoot,
23016
23493
  relativeDirPath: paths.relativeDirPath,
@@ -23020,7 +23497,9 @@ var KiroMcp = class KiroMcp extends ToolMcp {
23020
23497
  });
23021
23498
  }
23022
23499
  toRulesyncMcp() {
23023
- return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: this.json.mcpServers ?? {} }, null, 2) });
23500
+ const mcpServers = this.json.mcpServers;
23501
+ const translated = isMcpServers(mcpServers) ? fromKiroMcpServers(mcpServers) : {};
23502
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: translated }, null, 2) });
23024
23503
  }
23025
23504
  validate() {
23026
23505
  return {
@@ -23906,7 +24385,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
23906
24385
  });
23907
24386
  }
23908
24387
  function disabledNamesOf(config) {
23909
- const mcpBlock = config && isRecord(config.mcp) ? config.mcp : {};
24388
+ const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
23910
24389
  return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
23911
24390
  }
23912
24391
  /**
@@ -24025,7 +24504,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
24025
24504
  const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
24026
24505
  const disabledNames = managedNames.filter((name) => {
24027
24506
  const server = servers[name];
24028
- return isRecord(server) && server.disabled === true;
24507
+ return isRecord$1(server) && server.disabled === true;
24029
24508
  });
24030
24509
  const existingContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)) ?? "";
24031
24510
  let existingParsed;
@@ -24039,7 +24518,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
24039
24518
  logger?.warn(`Skipping the Rovo Dev mcp.disabledMcpServers update: ${formatError(error)}`);
24040
24519
  return [];
24041
24520
  }
24042
- const existingMcp = isRecord(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
24521
+ const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
24043
24522
  const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
24044
24523
  const managedNameSet = new Set(managedNames);
24045
24524
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
@@ -24231,7 +24710,7 @@ function deriveTransportAllowlist(servers) {
24231
24710
  http: false
24232
24711
  };
24233
24712
  for (const server of Object.values(servers)) {
24234
- if (!isRecord(server)) continue;
24713
+ if (!isRecord$1(server)) continue;
24235
24714
  const transport = transportOf(server);
24236
24715
  if (transport) allowlist[transport] = true;
24237
24716
  }
@@ -27866,13 +28345,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
27866
28345
  WebFetch: "webfetch",
27867
28346
  Mcp: "mcp"
27868
28347
  };
27869
- const MCP_CANONICAL_PREFIX$1 = "mcp__";
28348
+ const MCP_CANONICAL_PREFIX$2 = "mcp__";
27870
28349
  /**
27871
28350
  * Returns true if the canonical category is the per-tool MCP form
27872
28351
  * `mcp__<server>__<tool>`.
27873
28352
  */
27874
28353
  function isMcpScopedCategory(canonical) {
27875
- return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
28354
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$2) && canonical.length > 5;
27876
28355
  }
27877
28356
  function toCursorType(canonical) {
27878
28357
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -27906,7 +28385,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
27906
28385
  if (match) {
27907
28386
  const server = match[1] ?? "*";
27908
28387
  const tool = match[2] ?? "*";
27909
- return `${MCP_CANONICAL_PREFIX$1}${server}__${tool}`;
28388
+ return `${MCP_CANONICAL_PREFIX$2}${server}__${tool}`;
27910
28389
  }
27911
28390
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
27912
28391
  }
@@ -28135,7 +28614,7 @@ function convertCursorToRulesyncPermissions(params) {
28135
28614
  const { type, pattern } = parseCursorPermissionEntry(entry);
28136
28615
  const canonical = toCanonicalCategory$1(type, pattern);
28137
28616
  if (!permission[canonical]) permission[canonical] = {};
28138
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
28617
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$2) ? "*" : pattern;
28139
28618
  permission[canonical][canonicalPattern] = action;
28140
28619
  }
28141
28620
  };
@@ -28270,14 +28749,14 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
28270
28749
  let settings;
28271
28750
  try {
28272
28751
  const parsed = JSON.parse(existingContent);
28273
- settings = isRecord(parsed) ? parsed : {};
28752
+ settings = isRecord$1(parsed) ? parsed : {};
28274
28753
  } catch (error) {
28275
28754
  throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
28276
28755
  }
28277
28756
  const config = rulesyncPermissions.getJson();
28278
28757
  const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
28279
28758
  const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
28280
- const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
28759
+ const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
28281
28760
  const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
28282
28761
  const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
28283
28762
  const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
@@ -28307,11 +28786,11 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
28307
28786
  let settings;
28308
28787
  try {
28309
28788
  const parsed = JSON.parse(this.getFileContent());
28310
- settings = isRecord(parsed) ? parsed : {};
28789
+ settings = isRecord$1(parsed) ? parsed : {};
28311
28790
  } catch (error) {
28312
28791
  throw new Error(`Failed to parse Devin permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28313
28792
  }
28314
- const permissions = isRecord(settings.permissions) ? settings.permissions : {};
28793
+ const permissions = isRecord$1(settings.permissions) ? settings.permissions : {};
28315
28794
  const config = convertDevinToRulesyncPermissions({
28316
28795
  allow: Array.isArray(permissions.allow) ? permissions.allow : [],
28317
28796
  ask: Array.isArray(permissions.ask) ? permissions.ask : [],
@@ -28660,7 +29139,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28660
29139
  } catch (error) {
28661
29140
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
28662
29141
  }
28663
- const config = isRecord(parsed) ? { ...parsed } : {};
29142
+ const config = isRecord$1(parsed) ? { ...parsed } : {};
28664
29143
  const userPermission = convertRulesyncToGoosePermissionConfig({
28665
29144
  config: rulesyncPermissions.getJson(),
28666
29145
  logger
@@ -28683,8 +29162,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28683
29162
  } catch (error) {
28684
29163
  throw new Error(`Failed to parse Goose permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28685
29164
  }
28686
- const config = isRecord(parsed) ? parsed : {};
28687
- const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
29165
+ const config = isRecord$1(parsed) ? parsed : {};
29166
+ const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
28688
29167
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
28689
29168
  }
28690
29169
  validate() {
@@ -28758,7 +29237,7 @@ const GROKCLI_UI_KEY = "ui";
28758
29237
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
28759
29238
  const GROKCLI_PERMISSION_KEY = "permission";
28760
29239
  const CATCH_ALL_PATTERN$2 = "*";
28761
- const MCP_CANONICAL_PREFIX = "mcp__";
29240
+ const MCP_CANONICAL_PREFIX$1 = "mcp__";
28762
29241
  const CATEGORY_TO_GROK_TOOL = {
28763
29242
  bash: "Bash",
28764
29243
  read: "Read",
@@ -28786,7 +29265,7 @@ const GROK_MCP_TOOL = "MCPTool";
28786
29265
  * concrete pattern emits `Tool(pattern)`.
28787
29266
  */
28788
29267
  function buildGrokEntry(category, pattern) {
28789
- if (category.startsWith(MCP_CANONICAL_PREFIX)) {
29268
+ if (category.startsWith(MCP_CANONICAL_PREFIX$1)) {
28790
29269
  const remainder = category.slice(5);
28791
29270
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
28792
29271
  }
@@ -28813,7 +29292,7 @@ function parseGrokEntry(entry) {
28813
29292
  inner = trimmed.slice(parenIndex + 1, -1).trim();
28814
29293
  }
28815
29294
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
28816
- category: `${MCP_CANONICAL_PREFIX}${inner}`,
29295
+ category: `${MCP_CANONICAL_PREFIX$1}${inner}`,
28817
29296
  pattern: CATCH_ALL_PATTERN$2
28818
29297
  } : {
28819
29298
  category: "mcp",
@@ -28911,7 +29390,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
28911
29390
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
28912
29391
  }
28913
29392
  const config = rulesyncPermissions.getJson();
28914
- const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
29393
+ const existingPermission = isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
28915
29394
  const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
28916
29395
  const permission = {
28917
29396
  ...existingPermission,
@@ -28920,7 +29399,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
28920
29399
  ask: buckets.ask
28921
29400
  };
28922
29401
  const uiPatch = global ? { [GROKCLI_UI_KEY]: {
28923
- ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
29402
+ ...isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
28924
29403
  [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
28925
29404
  } } : {};
28926
29405
  return new GrokcliPermissions({
@@ -28949,7 +29428,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
28949
29428
  } catch (error) {
28950
29429
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28951
29430
  }
28952
- const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
29431
+ const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
28953
29432
  const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
28954
29433
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
28955
29434
  }
@@ -29046,7 +29525,7 @@ function parseGrokPermissionArrays(permission) {
29046
29525
  * `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
29047
29526
  */
29048
29527
  function legacyModeAction(parsed) {
29049
- return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
29528
+ return (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
29050
29529
  }
29051
29530
  /**
29052
29531
  * Collapse a rulesync permissions config into Grok's single coarse mode.
@@ -29102,12 +29581,12 @@ function withoutKey(record, key) {
29102
29581
  return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
29103
29582
  }
29104
29583
  function buildHermesOverride(config, provenance) {
29105
- const base = isRecord(provenance.hermes) ? { ...provenance.hermes } : {};
29106
- const approvalsOverride = withoutKey(isRecord(config.approvals) ? config.approvals : {}, "deny");
29584
+ const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
29585
+ const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
29107
29586
  if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
29108
29587
  else delete base.approvals;
29109
- const security = isRecord(config.security) ? { ...config.security } : {};
29110
- const blocklist = isRecord(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
29588
+ const security = isRecord$1(config.security) ? { ...config.security } : {};
29589
+ const blocklist = isRecord$1(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
29111
29590
  if (blocklist?.enabled === true) {
29112
29591
  delete blocklist.domains;
29113
29592
  delete blocklist.enabled;
@@ -29116,7 +29595,7 @@ function buildHermesOverride(config, provenance) {
29116
29595
  else delete security.website_blocklist;
29117
29596
  if (Object.keys(security).length > 0) base.security = security;
29118
29597
  else delete base.security;
29119
- for (const key of ["skills", "memory"]) if (isRecord(config[key])) base[key] = config[key];
29598
+ for (const key of ["skills", "memory"]) if (isRecord$1(config[key])) base[key] = config[key];
29120
29599
  else delete base[key];
29121
29600
  return base;
29122
29601
  }
@@ -29188,7 +29667,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
29188
29667
  format: "yaml",
29189
29668
  fileContent: this.getFileContent()
29190
29669
  });
29191
- const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
29670
+ const permissionsRoot = isRecord$1(config.permissions) ? config.permissions : {};
29192
29671
  const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(permissionsRoot.rulesync);
29193
29672
  const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
29194
29673
  const permission = clonePermissionBlock(provenance.permission);
@@ -29196,14 +29675,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
29196
29675
  permission,
29197
29676
  commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
29198
29677
  });
29199
- const approvals = isRecord(config.approvals) ? config.approvals : {};
29678
+ const approvals = isRecord$1(config.approvals) ? config.approvals : {};
29200
29679
  reconcileNativeDenies({
29201
29680
  permission,
29202
29681
  category: "bash",
29203
29682
  patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
29204
29683
  });
29205
- const security = isRecord(config.security) ? config.security : {};
29206
- const websiteBlocklist = isRecord(security.website_blocklist) ? security.website_blocklist : {};
29684
+ const security = isRecord$1(config.security) ? config.security : {};
29685
+ const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
29207
29686
  reconcileNativeDenies({
29208
29687
  permission,
29209
29688
  category: "webfetch",
@@ -29878,8 +30357,8 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
29878
30357
  existing = void 0;
29879
30358
  }
29880
30359
  const merged = {
29881
- ...isRecord(existing) ? existing : {},
29882
- ...isRecord(patch.tools) ? patch.tools : {}
30360
+ ...isRecord$1(existing) ? existing : {},
30361
+ ...isRecord$1(patch.tools) ? patch.tools : {}
29883
30362
  };
29884
30363
  if (Object.keys(merged).length === 0) return;
29885
30364
  warnAboutMistypedToolLists(merged);
@@ -29903,7 +30382,7 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
29903
30382
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#tools
29904
30383
  */
29905
30384
  function buildKimiCodeToolsSection(tools) {
29906
- if (!isRecord(tools)) return;
30385
+ if (!isRecord$1(tools)) return;
29907
30386
  const section = Object.fromEntries(Object.entries(tools).filter(([key, value]) => key === "enabled" || key === "disabled" ? isStringList(value) : true));
29908
30387
  return Object.keys(section).length > 0 ? section : void 0;
29909
30388
  }
@@ -30002,7 +30481,7 @@ function preserveKimiCodeRules(rules) {
30002
30481
  nativeRules
30003
30482
  };
30004
30483
  for (const raw of rules) {
30005
- if (!isRecord(raw)) continue;
30484
+ if (!isRecord$1(raw)) continue;
30006
30485
  const decision = raw.decision;
30007
30486
  const pattern = raw.pattern;
30008
30487
  if (decision !== "allow" && decision !== "ask" && decision !== "deny" || typeof pattern !== "string") continue;
@@ -30117,7 +30596,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
30117
30596
  format: "toml",
30118
30597
  fileContent: this.getFileContent()
30119
30598
  });
30120
- const { permission, nativeRules } = preserveKimiCodeRules((isRecord(config.permission) ? config.permission : {}).rules);
30599
+ const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
30121
30600
  const defaultPermissionMode = config.default_permission_mode;
30122
30601
  const tools = buildKimiCodeToolsSection(config.tools);
30123
30602
  const toolOverride = {
@@ -31394,7 +31873,7 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
31394
31873
  } catch (error) {
31395
31874
  throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
31396
31875
  }
31397
- const config = isRecord(parsed) ? { ...parsed } : {};
31876
+ const config = isRecord$1(parsed) ? { ...parsed } : {};
31398
31877
  const rulesyncConfig = rulesyncPermissions.getJson();
31399
31878
  const toolPermissions = convertRulesyncToRovodevToolPermissions({
31400
31879
  config: rulesyncConfig,
@@ -31431,8 +31910,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
31431
31910
  } catch (error) {
31432
31911
  throw new Error(`Failed to parse Rovodev permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
31433
31912
  }
31434
- const config = isRecord(parsed) ? parsed : {};
31435
- const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
31913
+ const config = isRecord$1(parsed) ? parsed : {};
31914
+ const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
31436
31915
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
31437
31916
  }
31438
31917
  validate() {
@@ -31459,14 +31938,14 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
31459
31938
  * keys it does not are kept as-is.
31460
31939
  */
31461
31940
  function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
31462
- const existingToolPermissions = isRecord(existing) ? { ...existing } : {};
31941
+ const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
31463
31942
  if (Object.keys(generated).length === 0 && sourceStatesRules) {
31464
- if (!isRecord(existing)) return;
31943
+ if (!isRecord$1(existing)) return;
31465
31944
  const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
31466
31945
  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(", ")}.` : `.`));
31467
31946
  return existingToolPermissions;
31468
31947
  }
31469
- const hasExistingToolsRecord = isRecord(existingToolPermissions.tools);
31948
+ const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
31470
31949
  const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
31471
31950
  warnAboutDroppedOwnedKeys({
31472
31951
  existingToolPermissions,
@@ -31510,7 +31989,7 @@ function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, gen
31510
31989
  */
31511
31990
  function stripPermissiveOwnedValues(toolPermissions) {
31512
31991
  const strippedKeys = [];
31513
- if (isRecord(toolPermissions.tools)) {
31992
+ if (isRecord$1(toolPermissions.tools)) {
31514
31993
  const tools = { ...toolPermissions.tools };
31515
31994
  for (const toolKey of MANAGED_TOOL_KEYS) if (tools[toolKey] === "allow") {
31516
31995
  delete tools[toolKey];
@@ -31532,14 +32011,14 @@ function stripPermissiveOwnedValues(toolPermissions) {
31532
32011
  strippedKeys.push("default");
31533
32012
  }
31534
32013
  const bash = toolPermissions.bash;
31535
- if (isRecord(bash)) {
32014
+ if (isRecord$1(bash)) {
31536
32015
  const stripped = { ...bash };
31537
32016
  if (stripped.default === "allow") {
31538
32017
  delete stripped.default;
31539
32018
  strippedKeys.push("bash.default");
31540
32019
  }
31541
32020
  if (Array.isArray(stripped.commands)) {
31542
- const kept = stripped.commands.filter((entry) => !(isRecord(entry) && entry.permission === "allow"));
32021
+ const kept = stripped.commands.filter((entry) => !(isRecord$1(entry) && entry.permission === "allow"));
31543
32022
  if (kept.length !== stripped.commands.length) strippedKeys.push("bash.commands");
31544
32023
  if (kept.length > 0) stripped.commands = kept;
31545
32024
  else delete stripped.commands;
@@ -31647,15 +32126,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
31647
32126
  const permission = {};
31648
32127
  if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
31649
32128
  const bash = toolPermissions.bash;
31650
- if (isRecord(bash)) {
32129
+ if (isRecord$1(bash)) {
31651
32130
  const bashRules = {};
31652
32131
  if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
31653
32132
  if (Array.isArray(bash.commands)) {
31654
- for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
32133
+ for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
31655
32134
  }
31656
32135
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
31657
32136
  }
31658
- const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
32137
+ const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
31659
32138
  const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
31660
32139
  for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
31661
32140
  const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
@@ -32370,11 +32849,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
32370
32849
  config,
32371
32850
  logger
32372
32851
  });
32373
- const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
32374
- const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
32852
+ const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
32853
+ const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
32375
32854
  const override = config.warp;
32376
- const executionProfileOverride = isRecord(override) && isRecord(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
32377
- if (isRecord(override)) {
32855
+ const executionProfileOverride = isRecord$1(override) && isRecord$1(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
32856
+ if (isRecord$1(override)) {
32378
32857
  const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
32379
32858
  Object.assign(profiles, legacyOverride);
32380
32859
  }
@@ -32409,10 +32888,10 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
32409
32888
  } catch (error) {
32410
32889
  throw new Error(`Failed to parse Warp permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
32411
32890
  }
32412
- const agents = isRecord(settings.agents) ? settings.agents : {};
32413
- const profiles = isRecord(agents.profiles) ? agents.profiles : {};
32414
- const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
32415
- const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
32891
+ const agents = isRecord$1(settings.agents) ? settings.agents : {};
32892
+ const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
32893
+ const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
32894
+ const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
32416
32895
  const config = convertWarpToRulesyncPermissions({
32417
32896
  allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
32418
32897
  deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
@@ -32459,12 +32938,12 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
32459
32938
  */
32460
32939
  function mergeIntoDefaultExecutionProfile({ agents, mergedAllow, mergedDeny, executionProfileOverride, logger }) {
32461
32940
  const hasOverrideKeys = executionProfileOverride !== void 0 && Object.keys(executionProfileOverride).length > 0;
32462
- if (!isRecord(agents[EXECUTION_PROFILES_KEY])) {
32941
+ if (!isRecord$1(agents[EXECUTION_PROFILES_KEY])) {
32463
32942
  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.");
32464
32943
  return;
32465
32944
  }
32466
32945
  const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
32467
- const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
32946
+ const defaultProfile = isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
32468
32947
  if (executionProfileOverride) Object.assign(defaultProfile, executionProfileOverride);
32469
32948
  if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
32470
32949
  else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
@@ -32538,9 +33017,13 @@ const ZedToolPermissionsSchema = z.looseObject({
32538
33017
  default: z.optional(ZedPermissionActionSchema),
32539
33018
  tools: z.optional(z.record(z.string(), ZedToolPermissionSchema))
32540
33019
  });
33020
+ /** Canonical per-tool MCP category prefix: `mcp__<server>__<tool>`. */
33021
+ const MCP_CANONICAL_PREFIX = "mcp__";
33022
+ /** Zed's per-tool MCP name prefix: `mcp:<server>:<tool>`. */
33023
+ const MCP_ZED_PREFIX = "mcp:";
32541
33024
  /**
32542
33025
  * Mapping from rulesync canonical tool category names to Zed agent tool names.
32543
- * Unknown names are passed through as-is (e.g. `mcp:<server>:<tool>` keys).
33026
+ * Unknown names are passed through as-is.
32544
33027
  */
32545
33028
  const CANONICAL_TO_ZED_TOOL_NAMES = {
32546
33029
  bash: "terminal",
@@ -32575,12 +33058,68 @@ const ZED_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
32575
33058
  ]);
32576
33059
  const isZedExcludedCategory = (category) => ZED_EXCLUDED_CANONICAL_CATEGORIES.has(category) || ZED_EXCLUDED_TOOL_NAMES.has(toZedToolName(category));
32577
33060
  const ZED_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZED_TOOL_NAMES).map(([k, v]) => [v, k]));
33061
+ /**
33062
+ * Zed addresses an MCP tool as `mcp:<server>:<tool>`, where rulesync's canonical
33063
+ * category is `mcp__<server>__<tool>`. Without this translation the canonical
33064
+ * spelling lands under a key Zed never looks up.
33065
+ *
33066
+ * Only the FIRST separator is split, matching the `cursor-permissions.ts`
33067
+ * precedent: upstream's `mcp_tool_id` concatenates the two names without
33068
+ * escaping either, so a tool called `create__issue` is legitimately
33069
+ * `mcp:github:create__issue`. Splitting every separator would rewrite that into
33070
+ * a third key on the next generate.
33071
+ *
33072
+ * @see https://zed.dev/docs/ai/tool-permissions
33073
+ */
32578
33074
  function toZedToolName(canonical) {
33075
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX)) {
33076
+ const [server, ...toolParts] = canonical.slice(5).split("__");
33077
+ const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
33078
+ return `${MCP_ZED_PREFIX}${address}`;
33079
+ }
32579
33080
  return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
32580
33081
  }
32581
33082
  function toCanonicalToolName(zedName) {
33083
+ if (zedName.startsWith(MCP_ZED_PREFIX)) {
33084
+ const [server, ...toolParts] = zedName.slice(4).split(":");
33085
+ const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
33086
+ return `${MCP_CANONICAL_PREFIX}${address}`;
33087
+ }
32582
33088
  return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
32583
33089
  }
33090
+ /**
33091
+ * Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
33092
+ * tool's text input, and it dispatches every MCP tool with a single empty input
33093
+ * (`&[String::new()]`, commented upstream as "MCP tools are gated only by tool
33094
+ * id (no per-input pattern matching)"). The regexes still run, but against `""`,
33095
+ * so a pattern-scoped rule silently does something other than what its author
33096
+ * meant — and a non-matching `always_allow` downgrades the outcome to confirm.
33097
+ * Only the category's `*` rule (Zed's per-tool `default`) is therefore emitted.
33098
+ */
33099
+ const isMcpZedToolName = (zedName) => zedName.startsWith(MCP_ZED_PREFIX);
33100
+ /**
33101
+ * Zed looks a tool up by exact key on the full `mcp:<server>:<tool>` triple, so
33102
+ * an address is inert unless it names both a concrete server and a concrete
33103
+ * tool: a missing half matches nothing, and so does a wildcard, since Zed does
33104
+ * no glob or prefix matching on the key.
33105
+ */
33106
+ function isInertMcpAddress(zedToolName) {
33107
+ if (!isMcpZedToolName(zedToolName)) return false;
33108
+ const [server, ...toolParts] = zedToolName.slice(4).split(":");
33109
+ const tool = toolParts.join(":");
33110
+ return !server || server === "*" || !tool || tool === "*";
33111
+ }
33112
+ /**
33113
+ * Strip pattern-scoped rules from an MCP category, warning once about the ones
33114
+ * dropped. Non-MCP categories are returned untouched.
33115
+ */
33116
+ function withoutInertMcpPatterns({ category, zedToolName, rules, logger }) {
33117
+ if (!isMcpZedToolName(zedToolName)) return rules;
33118
+ const scopedPatterns = Object.keys(rules).filter((pattern) => pattern !== "*");
33119
+ if (scopedPatterns.length === 0) return rules;
33120
+ 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.`);
33121
+ return Object.fromEntries(Object.entries(rules).filter(([pattern]) => pattern === "*"));
33122
+ }
32584
33123
  const CANONICAL_TO_ZED_ACTION = {
32585
33124
  allow: "allow",
32586
33125
  ask: "confirm",
@@ -32623,6 +33162,53 @@ function buildZedToolPermission(rules) {
32623
33162
  if (alwaysConfirm.length > 0) tool.always_confirm = alwaysConfirm;
32624
33163
  return Object.keys(tool).length > 0 ? tool : null;
32625
33164
  }
33165
+ /**
33166
+ * Split a canonical permission block into the Zed shapes it maps onto, plus the
33167
+ * categories the caller should report as dropped.
33168
+ *
33169
+ * The canonical `*` category is the all-tools catch-all. Zed's counterpart is
33170
+ * `agent.tool_permissions.default` (rung 6 of its precedence ladder), not a
33171
+ * `tools["*"]` entry — `*` is not a Zed tool name, so writing one produces a
33172
+ * rule Zed silently ignores. Only the category's own `*` pattern can be
33173
+ * expressed there: Zed's global default carries no pattern list, so
33174
+ * pattern-scoped rules in the `*` category are dropped with a warning instead of
33175
+ * being emitted as inert config.
33176
+ */
33177
+ function buildZedToolPermissions({ permission, logger }) {
33178
+ let managedDefault;
33179
+ const managedTools = {};
33180
+ const excludedCategories = [];
33181
+ const inertMcpCategories = [];
33182
+ for (const [category, rules] of Object.entries(permission)) {
33183
+ if (category === "*") {
33184
+ for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
33185
+ 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.`);
33186
+ continue;
33187
+ }
33188
+ if (isZedExcludedCategory(category)) {
33189
+ if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
33190
+ continue;
33191
+ }
33192
+ const zedToolName = toZedToolName(category);
33193
+ if (isInertMcpAddress(zedToolName)) {
33194
+ inertMcpCategories.push(category);
33195
+ continue;
33196
+ }
33197
+ const tool = buildZedToolPermission(withoutInertMcpPatterns({
33198
+ category,
33199
+ zedToolName,
33200
+ rules,
33201
+ logger
33202
+ }));
33203
+ if (tool) managedTools[zedToolName] = tool;
33204
+ }
33205
+ return {
33206
+ managedDefault,
33207
+ managedTools,
33208
+ excludedCategories,
33209
+ inertMcpCategories
33210
+ };
33211
+ }
32626
33212
  function asRecord(value) {
32627
33213
  if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
32628
33214
  return Object.fromEntries(Object.entries(value));
@@ -32693,25 +33279,15 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
32693
33279
  const agent = asRecord(settings.agent);
32694
33280
  const toolPermissions = asRecord(agent.tool_permissions);
32695
33281
  const existingTools = asRecord(toolPermissions.tools);
32696
- let managedDefault;
32697
- const managedTools = {};
32698
- const excludedCategories = [];
32699
- for (const [category, rules] of Object.entries(config.permission)) {
32700
- if (category === "*") {
32701
- for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
32702
- 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.`);
32703
- continue;
32704
- }
32705
- if (isZedExcludedCategory(category)) {
32706
- if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
32707
- continue;
32708
- }
32709
- const tool = buildZedToolPermission(rules);
32710
- if (tool) managedTools[toZedToolName(category)] = tool;
32711
- }
33282
+ const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
33283
+ permission: config.permission,
33284
+ logger
33285
+ });
32712
33286
  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\`.`);
33287
+ 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.`);
32713
33288
  const managedToolNames = new Set(Object.keys(managedTools));
32714
33289
  if ("*" in config.permission) managedToolNames.add("*");
33290
+ for (const toolName of Object.keys(existingTools)) if (toolName.startsWith(MCP_CANONICAL_PREFIX)) managedToolNames.add(toolName);
32715
33291
  const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
32716
33292
  return new ZedPermissions({
32717
33293
  outputRoot,
@@ -34912,18 +35488,10 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
34912
35488
  };
34913
35489
  }
34914
35490
  toRulesyncSkill() {
34915
- const frontmatter = this.getFrontmatter();
34916
- const copilotSection = {
34917
- ...frontmatter.license !== void 0 && { license: frontmatter.license },
34918
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
34919
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
34920
- ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
34921
- ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
34922
- ...frontmatter.context !== void 0 && { context: frontmatter.context }
34923
- };
35491
+ const { name, description, ...copilotSection } = this.getFrontmatter();
34924
35492
  const rulesyncFrontmatter = {
34925
- name: frontmatter.name,
34926
- description: frontmatter.description,
35493
+ name,
35494
+ description,
34927
35495
  targets: ["*"],
34928
35496
  ...Object.keys(copilotSection).length > 0 && { copilot: copilotSection }
34929
35497
  };
@@ -34950,15 +35518,13 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
34950
35518
  rootFrontmatter: rulesyncFrontmatter,
34951
35519
  section: copilotSection
34952
35520
  });
35521
+ const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotFields } = copilotSection ?? {};
34953
35522
  const copilotFrontmatter = {
35523
+ ...copilotFields,
34954
35524
  name: rulesyncFrontmatter.name,
34955
35525
  description: rulesyncFrontmatter.description,
34956
- ...copilotSection?.license !== void 0 && { license: copilotSection.license },
34957
- ...copilotSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotSection["allowed-tools"] },
34958
- ...copilotSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotSection["argument-hint"] },
34959
35526
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
34960
- ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
34961
- ...copilotSection?.context !== void 0 && { context: copilotSection.context }
35527
+ ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
34962
35528
  };
34963
35529
  return new CopilotSkill({
34964
35530
  outputRoot,
@@ -35077,17 +35643,10 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
35077
35643
  };
35078
35644
  }
35079
35645
  toRulesyncSkill() {
35080
- const frontmatter = this.getFrontmatter();
35081
- const copilotcliSection = {
35082
- ...frontmatter.license !== void 0 && { license: frontmatter.license },
35083
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
35084
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
35085
- ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
35086
- ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
35087
- };
35646
+ const { name, description, ...copilotcliSection } = this.getFrontmatter();
35088
35647
  const rulesyncFrontmatter = {
35089
- name: frontmatter.name,
35090
- description: frontmatter.description,
35648
+ name,
35649
+ description,
35091
35650
  targets: ["*"],
35092
35651
  ...Object.keys(copilotcliSection).length > 0 && { copilotcli: copilotcliSection }
35093
35652
  };
@@ -35114,12 +35673,11 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
35114
35673
  rootFrontmatter: rulesyncFrontmatter,
35115
35674
  section: copilotcliSection
35116
35675
  });
35676
+ const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotcliFields } = copilotcliSection ?? {};
35117
35677
  const copilotcliFrontmatter = {
35678
+ ...copilotcliFields,
35118
35679
  name: rulesyncFrontmatter.name,
35119
35680
  description: rulesyncFrontmatter.description,
35120
- ...copilotcliSection?.license !== void 0 && { license: copilotcliSection.license },
35121
- ...copilotcliSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotcliSection["allowed-tools"] },
35122
- ...copilotcliSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotcliSection["argument-hint"] },
35123
35681
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
35124
35682
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
35125
35683
  };
@@ -36593,7 +37151,10 @@ var KimiCodeSkill = class KimiCodeSkill extends ToolSkill {
36593
37151
  //#region src/features/skills/kiro-skill.ts
36594
37152
  const KiroSkillFrontmatterSchema = z.looseObject({
36595
37153
  name: z.string(),
36596
- description: z.string()
37154
+ description: z.string(),
37155
+ license: z.optional(z.string()),
37156
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
37157
+ metadata: z.optional(z.looseObject({}))
36597
37158
  });
36598
37159
  /**
36599
37160
  * Represents a Kiro skill directory.
@@ -36647,11 +37208,12 @@ var KiroSkill = class KiroSkill extends ToolSkill {
36647
37208
  };
36648
37209
  }
36649
37210
  toRulesyncSkill() {
36650
- const frontmatter = this.getFrontmatter();
37211
+ const { name, description, ...kiroSection } = this.getFrontmatter();
36651
37212
  const rulesyncFrontmatter = {
36652
- name: frontmatter.name,
36653
- description: frontmatter.description,
36654
- targets: ["*"]
37213
+ name,
37214
+ description,
37215
+ targets: ["*"],
37216
+ ...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
36655
37217
  };
36656
37218
  return new RulesyncSkill({
36657
37219
  outputRoot: this.outputRoot,
@@ -36668,6 +37230,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
36668
37230
  const settablePaths = KiroSkill.getSettablePaths({ global });
36669
37231
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
36670
37232
  const kiroFrontmatter = {
37233
+ ...rulesyncFrontmatter.kiro,
36671
37234
  name: rulesyncFrontmatter.name,
36672
37235
  description: rulesyncFrontmatter.description
36673
37236
  };
@@ -43236,14 +43799,14 @@ var ZoocodeSubagent = class extends RooSubagent {
43236
43799
  static toRooMode(rulesyncSubagent) {
43237
43800
  const mode = super.toRooMode(rulesyncSubagent);
43238
43801
  const frontmatter = rulesyncSubagent.getFrontmatter();
43239
- const zoocodeSection = isRecord(frontmatter.zoocode) ? frontmatter.zoocode : {};
43802
+ const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
43240
43803
  if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
43241
43804
  return mode;
43242
43805
  }
43243
43806
  toRulesyncSubagents() {
43244
43807
  return super.toRulesyncSubagents().map((subagent) => {
43245
43808
  const frontmatter = subagent.getFrontmatter();
43246
- const { allowedMcpServers, ...restRooSection } = isRecord(frontmatter.roo) ? { ...frontmatter.roo } : {};
43809
+ const { allowedMcpServers, ...restRooSection } = isRecord$1(frontmatter.roo) ? { ...frontmatter.roo } : {};
43247
43810
  const rebuilt = {
43248
43811
  ...frontmatter,
43249
43812
  targets: ["zoocode"],
@@ -51315,4 +51878,4 @@ async function importChecksCore(params) {
51315
51878
  //#endregion
51316
51879
  export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
51317
51880
 
51318
- //# sourceMappingURL=import-zyfw1Cq_.js.map
51881
+ //# sourceMappingURL=import-KXnvmbzr.js.map