rulesync 7.12.0 → 7.12.2

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.
package/dist/index.cjs CHANGED
@@ -934,7 +934,7 @@ var ToolCommand = class extends AiFile {
934
934
 
935
935
  // src/features/commands/simulated-command.ts
936
936
  var SimulatedCommandFrontmatterSchema = import_mini4.z.object({
937
- description: import_mini4.z.string()
937
+ description: import_mini4.z.optional(import_mini4.z.string())
938
938
  });
939
939
  var SimulatedCommand = class _SimulatedCommand extends ToolCommand {
940
940
  frontmatter;
@@ -1121,7 +1121,7 @@ var RulesyncFile = class extends AiFile {
1121
1121
  // src/features/commands/rulesync-command.ts
1122
1122
  var RulesyncCommandFrontmatterSchema = import_mini5.z.looseObject({
1123
1123
  targets: import_mini5.z._default(RulesyncTargetsSchema, ["*"]),
1124
- description: import_mini5.z.string()
1124
+ description: import_mini5.z.optional(import_mini5.z.string())
1125
1125
  });
1126
1126
  var RulesyncCommand = class _RulesyncCommand extends RulesyncFile {
1127
1127
  frontmatter;
@@ -1209,7 +1209,7 @@ var AntigravityWorkflowFrontmatterSchema = import_mini6.z.looseObject({
1209
1209
  turbo: import_mini6.z.optional(import_mini6.z.boolean())
1210
1210
  });
1211
1211
  var AntigravityCommandFrontmatterSchema = import_mini6.z.looseObject({
1212
- description: import_mini6.z.string(),
1212
+ description: import_mini6.z.optional(import_mini6.z.string()),
1213
1213
  // Support for workflow-specific configuration
1214
1214
  ...AntigravityWorkflowFrontmatterSchema.shape
1215
1215
  });
@@ -1382,7 +1382,7 @@ ${body}${turboDirective}`;
1382
1382
  var import_node_path9 = require("path");
1383
1383
  var import_mini7 = require("zod/mini");
1384
1384
  var ClaudecodeCommandFrontmatterSchema = import_mini7.z.looseObject({
1385
- description: import_mini7.z.string(),
1385
+ description: import_mini7.z.optional(import_mini7.z.string()),
1386
1386
  "allowed-tools": import_mini7.z.optional(import_mini7.z.union([import_mini7.z.string(), import_mini7.z.array(import_mini7.z.string())])),
1387
1387
  "argument-hint": import_mini7.z.optional(import_mini7.z.string()),
1388
1388
  model: import_mini7.z.optional(import_mini7.z.string()),
@@ -1537,8 +1537,7 @@ var ClineCommand = class _ClineCommand extends ToolCommand {
1537
1537
  }
1538
1538
  toRulesyncCommand() {
1539
1539
  const rulesyncFrontmatter = {
1540
- targets: ["*"],
1541
- description: ""
1540
+ targets: ["*"]
1542
1541
  };
1543
1542
  return new RulesyncCommand({
1544
1543
  baseDir: process.cwd(),
@@ -1623,8 +1622,7 @@ var CodexcliCommand = class _CodexcliCommand extends ToolCommand {
1623
1622
  }
1624
1623
  toRulesyncCommand() {
1625
1624
  const rulesyncFrontmatter = {
1626
- targets: ["*"],
1627
- description: ""
1625
+ targets: ["*"]
1628
1626
  };
1629
1627
  return new RulesyncCommand({
1630
1628
  baseDir: ".",
@@ -1702,7 +1700,7 @@ var import_node_path12 = require("path");
1702
1700
  var import_mini8 = require("zod/mini");
1703
1701
  var CopilotCommandFrontmatterSchema = import_mini8.z.looseObject({
1704
1702
  mode: import_mini8.z.optional(import_mini8.z.string()),
1705
- description: import_mini8.z.string()
1703
+ description: import_mini8.z.optional(import_mini8.z.string())
1706
1704
  });
1707
1705
  var CopilotCommand = class _CopilotCommand extends ToolCommand {
1708
1706
  frontmatter;
@@ -1885,7 +1883,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
1885
1883
  return this.frontmatter;
1886
1884
  }
1887
1885
  toRulesyncCommand() {
1888
- const { description = "", ...restFields } = this.frontmatter;
1886
+ const { description, ...restFields } = this.frontmatter;
1889
1887
  const rulesyncFrontmatter = {
1890
1888
  targets: ["*"],
1891
1889
  description,
@@ -2079,7 +2077,7 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
2079
2077
  }
2080
2078
  return {
2081
2079
  ...result.data,
2082
- description: result.data.description || ""
2080
+ description: result.data.description
2083
2081
  };
2084
2082
  } catch (error) {
2085
2083
  throw new Error(
@@ -2101,7 +2099,7 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
2101
2099
  const { description, prompt: _prompt, ...restFields } = this.frontmatter;
2102
2100
  const rulesyncFrontmatter = {
2103
2101
  targets: ["geminicli"],
2104
- description: description ?? "",
2102
+ description,
2105
2103
  // Preserve extra fields in geminicli section (excluding prompt which is the body)
2106
2104
  ...Object.keys(restFields).length > 0 && { geminicli: restFields }
2107
2105
  };
@@ -2130,8 +2128,9 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
2130
2128
  prompt: rulesyncCommand.getBody(),
2131
2129
  ...geminicliFields
2132
2130
  };
2133
- const tomlContent = `description = "${geminiFrontmatter.description}"
2134
- prompt = """
2131
+ const descriptionLine = geminiFrontmatter.description !== void 0 ? `description = "${geminiFrontmatter.description}"
2132
+ ` : "";
2133
+ const tomlContent = `${descriptionLine}prompt = """
2135
2134
  ${geminiFrontmatter.prompt}
2136
2135
  """`;
2137
2136
  const paths = this.getSettablePaths({ global });
@@ -2201,8 +2200,7 @@ var KiloCommand = class _KiloCommand extends ToolCommand {
2201
2200
  }
2202
2201
  toRulesyncCommand() {
2203
2202
  const rulesyncFrontmatter = {
2204
- targets: ["*"],
2205
- description: ""
2203
+ targets: ["*"]
2206
2204
  };
2207
2205
  return new RulesyncCommand({
2208
2206
  baseDir: process.cwd(),
@@ -2282,8 +2280,7 @@ var KiroCommand = class _KiroCommand extends ToolCommand {
2282
2280
  }
2283
2281
  toRulesyncCommand() {
2284
2282
  const rulesyncFrontmatter = {
2285
- targets: ["*"],
2286
- description: ""
2283
+ targets: ["*"]
2287
2284
  };
2288
2285
  return new RulesyncCommand({
2289
2286
  baseDir: process.cwd(),
@@ -2357,7 +2354,7 @@ var KiroCommand = class _KiroCommand extends ToolCommand {
2357
2354
  var import_node_path18 = require("path");
2358
2355
  var import_mini11 = require("zod/mini");
2359
2356
  var OpenCodeCommandFrontmatterSchema = import_mini11.z.looseObject({
2360
- description: import_mini11.z.string(),
2357
+ description: import_mini11.z.optional(import_mini11.z.string()),
2361
2358
  agent: (0, import_mini11.optional)(import_mini11.z.string()),
2362
2359
  subtask: (0, import_mini11.optional)(import_mini11.z.boolean()),
2363
2360
  model: (0, import_mini11.optional)(import_mini11.z.string())
@@ -2497,7 +2494,7 @@ var OpenCodeCommand = class _OpenCodeCommand extends ToolCommand {
2497
2494
  var import_node_path19 = require("path");
2498
2495
  var import_mini12 = require("zod/mini");
2499
2496
  var RooCommandFrontmatterSchema = import_mini12.z.looseObject({
2500
- description: import_mini12.z.string(),
2497
+ description: import_mini12.z.optional(import_mini12.z.string()),
2501
2498
  "argument-hint": (0, import_mini12.optional)(import_mini12.z.string())
2502
2499
  });
2503
2500
  var RooCommand = class _RooCommand extends ToolCommand {
@@ -3036,7 +3033,7 @@ var HookDefinitionSchema = import_mini14.z.looseObject({
3036
3033
  type: import_mini14.z.optional(import_mini14.z.enum(["command", "prompt"])),
3037
3034
  timeout: import_mini14.z.optional(import_mini14.z.number()),
3038
3035
  matcher: import_mini14.z.optional(safeString),
3039
- prompt: import_mini14.z.optional(import_mini14.z.string()),
3036
+ prompt: import_mini14.z.optional(safeString),
3040
3037
  loop_limit: import_mini14.z.optional(import_mini14.z.nullable(import_mini14.z.number())),
3041
3038
  name: import_mini14.z.optional(safeString),
3042
3039
  description: import_mini14.z.optional(safeString)
@@ -3088,7 +3085,7 @@ var OPENCODE_HOOK_EVENTS = [
3088
3085
  var COPILOT_HOOK_EVENTS = [
3089
3086
  "sessionStart",
3090
3087
  "sessionEnd",
3091
- "afterSubmitPrompt",
3088
+ "beforeSubmitPrompt",
3092
3089
  "preToolUse",
3093
3090
  "postToolUse",
3094
3091
  "afterError"
@@ -3199,7 +3196,7 @@ var CANONICAL_TO_OPENCODE_EVENT_NAMES = {
3199
3196
  var CANONICAL_TO_COPILOT_EVENT_NAMES = {
3200
3197
  sessionStart: "sessionStart",
3201
3198
  sessionEnd: "sessionEnd",
3202
- afterSubmitPrompt: "userPromptSubmitted",
3199
+ beforeSubmitPrompt: "userPromptSubmitted",
3203
3200
  preToolUse: "preToolUse",
3204
3201
  postToolUse: "postToolUse",
3205
3202
  afterError: "errorOccurred"
@@ -3227,6 +3224,107 @@ var GEMINICLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(
3227
3224
  // src/features/hooks/claudecode-hooks.ts
3228
3225
  var import_node_path22 = require("path");
3229
3226
 
3227
+ // src/features/hooks/tool-hooks-converter.ts
3228
+ function isToolMatcherEntry(x) {
3229
+ if (x === null || typeof x !== "object") {
3230
+ return false;
3231
+ }
3232
+ if ("matcher" in x && typeof x.matcher !== "string") {
3233
+ return false;
3234
+ }
3235
+ if ("hooks" in x && !Array.isArray(x.hooks)) {
3236
+ return false;
3237
+ }
3238
+ return true;
3239
+ }
3240
+ function canonicalToToolHooks({
3241
+ config,
3242
+ toolOverrideHooks,
3243
+ converterConfig
3244
+ }) {
3245
+ const supported = new Set(converterConfig.supportedEvents);
3246
+ const sharedHooks = {};
3247
+ for (const [event, defs] of Object.entries(config.hooks)) {
3248
+ if (supported.has(event)) {
3249
+ sharedHooks[event] = defs;
3250
+ }
3251
+ }
3252
+ const effectiveHooks = {
3253
+ ...sharedHooks,
3254
+ ...toolOverrideHooks
3255
+ };
3256
+ const result = {};
3257
+ for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
3258
+ const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
3259
+ const byMatcher = /* @__PURE__ */ new Map();
3260
+ for (const def of definitions) {
3261
+ const key = def.matcher ?? "";
3262
+ const list = byMatcher.get(key);
3263
+ if (list) list.push(def);
3264
+ else byMatcher.set(key, [def]);
3265
+ }
3266
+ const entries = [];
3267
+ for (const [matcherKey, defs] of byMatcher) {
3268
+ const hooks = defs.map((def) => {
3269
+ const commandText = def.command;
3270
+ const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
3271
+ const shouldPrefix = typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith("."));
3272
+ const command = shouldPrefix && typeof trimmedCommand === "string" ? `${converterConfig.projectDirVar}/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
3273
+ return {
3274
+ type: def.type ?? "command",
3275
+ ...command !== void 0 && command !== null && { command },
3276
+ ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
3277
+ ...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt }
3278
+ };
3279
+ });
3280
+ entries.push(matcherKey ? { matcher: matcherKey, hooks } : { hooks });
3281
+ }
3282
+ result[toolEventName] = entries;
3283
+ }
3284
+ return result;
3285
+ }
3286
+ function toolHooksToCanonical({
3287
+ hooks,
3288
+ converterConfig
3289
+ }) {
3290
+ if (hooks === null || hooks === void 0 || typeof hooks !== "object") {
3291
+ return {};
3292
+ }
3293
+ const canonical = {};
3294
+ for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
3295
+ const eventName = converterConfig.toolToCanonicalEventNames[toolEventName] ?? toolEventName;
3296
+ if (!Array.isArray(matcherEntries)) continue;
3297
+ const defs = [];
3298
+ for (const rawEntry of matcherEntries) {
3299
+ if (!isToolMatcherEntry(rawEntry)) continue;
3300
+ const hookDefs = rawEntry.hooks ?? [];
3301
+ for (const h of hookDefs) {
3302
+ const cmd = typeof h.command === "string" ? h.command : void 0;
3303
+ const command = typeof cmd === "string" && cmd.includes(`${converterConfig.projectDirVar}/`) ? cmd.replace(
3304
+ new RegExp(
3305
+ `^${converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/?`
3306
+ ),
3307
+ "./"
3308
+ ) : cmd;
3309
+ const hookType = h.type === "command" || h.type === "prompt" ? h.type : "command";
3310
+ const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
3311
+ const prompt = typeof h.prompt === "string" ? h.prompt : void 0;
3312
+ defs.push({
3313
+ type: hookType,
3314
+ ...command !== void 0 && command !== null && { command },
3315
+ ...timeout !== void 0 && timeout !== null && { timeout },
3316
+ ...prompt !== void 0 && prompt !== null && { prompt },
3317
+ ...rawEntry.matcher !== void 0 && rawEntry.matcher !== null && rawEntry.matcher !== "" && { matcher: rawEntry.matcher }
3318
+ });
3319
+ }
3320
+ }
3321
+ if (defs.length > 0) {
3322
+ canonical[eventName] = defs;
3323
+ }
3324
+ }
3325
+ return canonical;
3326
+ }
3327
+
3230
3328
  // src/types/tool-file.ts
3231
3329
  var ToolFile = class extends AiFile {
3232
3330
  };
@@ -3317,91 +3415,13 @@ var ToolHooks = class extends ToolFile {
3317
3415
  };
3318
3416
 
3319
3417
  // src/features/hooks/claudecode-hooks.ts
3320
- function canonicalToClaudeHooks(config) {
3321
- const claudeSupported = new Set(CLAUDE_HOOK_EVENTS);
3322
- const sharedHooks = {};
3323
- for (const [event, defs] of Object.entries(config.hooks)) {
3324
- if (claudeSupported.has(event)) {
3325
- sharedHooks[event] = defs;
3326
- }
3327
- }
3328
- const effectiveHooks = {
3329
- ...sharedHooks,
3330
- ...config.claudecode?.hooks
3331
- };
3332
- const claude = {};
3333
- for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
3334
- const claudeEventName = CANONICAL_TO_CLAUDE_EVENT_NAMES[eventName] ?? eventName;
3335
- const byMatcher = /* @__PURE__ */ new Map();
3336
- for (const def of definitions) {
3337
- const key = def.matcher ?? "";
3338
- const list = byMatcher.get(key);
3339
- if (list) list.push(def);
3340
- else byMatcher.set(key, [def]);
3341
- }
3342
- const entries = [];
3343
- for (const [matcherKey, defs] of byMatcher) {
3344
- const hooks = defs.map((def) => {
3345
- const command = def.command !== void 0 && def.command !== null && !def.command.startsWith("$") ? `$CLAUDE_PROJECT_DIR/${def.command.replace(/^\.\//, "")}` : def.command;
3346
- return {
3347
- type: def.type ?? "command",
3348
- ...command !== void 0 && command !== null && { command },
3349
- ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
3350
- ...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt }
3351
- };
3352
- });
3353
- entries.push(matcherKey ? { matcher: matcherKey, hooks } : { hooks });
3354
- }
3355
- claude[claudeEventName] = entries;
3356
- }
3357
- return claude;
3358
- }
3359
- function isClaudeMatcherEntry(x) {
3360
- if (x === null || typeof x !== "object") {
3361
- return false;
3362
- }
3363
- if ("matcher" in x && typeof x.matcher !== "string") {
3364
- return false;
3365
- }
3366
- if ("hooks" in x && !Array.isArray(x.hooks)) {
3367
- return false;
3368
- }
3369
- return true;
3370
- }
3371
- function claudeHooksToCanonical(claudeHooks) {
3372
- if (claudeHooks === null || claudeHooks === void 0 || typeof claudeHooks !== "object") {
3373
- return {};
3374
- }
3375
- const canonical = {};
3376
- for (const [claudeEventName, matcherEntries] of Object.entries(claudeHooks)) {
3377
- const eventName = CLAUDE_TO_CANONICAL_EVENT_NAMES[claudeEventName] ?? claudeEventName;
3378
- if (!Array.isArray(matcherEntries)) continue;
3379
- const defs = [];
3380
- for (const rawEntry of matcherEntries) {
3381
- if (!isClaudeMatcherEntry(rawEntry)) continue;
3382
- const entry = rawEntry;
3383
- const hooks = entry.hooks ?? [];
3384
- for (const h of hooks) {
3385
- const cmd = typeof h.command === "string" ? h.command : void 0;
3386
- const command = typeof cmd === "string" && cmd.includes("$CLAUDE_PROJECT_DIR/") ? cmd.replace(/^\$CLAUDE_PROJECT_DIR\/?/, "./") : cmd;
3387
- const hookType = h.type === "command" || h.type === "prompt" ? h.type : "command";
3388
- const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
3389
- const prompt = typeof h.prompt === "string" ? h.prompt : void 0;
3390
- defs.push({
3391
- type: hookType,
3392
- ...command !== void 0 && command !== null && { command },
3393
- ...timeout !== void 0 && timeout !== null && { timeout },
3394
- ...prompt !== void 0 && prompt !== null && { prompt },
3395
- ...entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && { matcher: entry.matcher }
3396
- });
3397
- }
3398
- }
3399
- if (defs.length > 0) {
3400
- canonical[eventName] = defs;
3401
- }
3402
- }
3403
- return canonical;
3404
- }
3418
+ var CLAUDE_CONVERTER_CONFIG = {
3419
+ supportedEvents: CLAUDE_HOOK_EVENTS,
3420
+ canonicalToToolEventNames: CANONICAL_TO_CLAUDE_EVENT_NAMES,
3421
+ toolToCanonicalEventNames: CLAUDE_TO_CANONICAL_EVENT_NAMES,
3422
+ projectDirVar: "$CLAUDE_PROJECT_DIR",
3423
+ prefixDotRelativeCommandsOnly: true
3424
+ };
3405
3425
  var ClaudecodeHooks = class _ClaudecodeHooks extends ToolHooks {
3406
3426
  constructor(params) {
3407
3427
  super({
@@ -3453,7 +3473,11 @@ var ClaudecodeHooks = class _ClaudecodeHooks extends ToolHooks {
3453
3473
  );
3454
3474
  }
3455
3475
  const config = rulesyncHooks.getJson();
3456
- const claudeHooks = canonicalToClaudeHooks(config);
3476
+ const claudeHooks = canonicalToToolHooks({
3477
+ config,
3478
+ toolOverrideHooks: config.claudecode?.hooks,
3479
+ converterConfig: CLAUDE_CONVERTER_CONFIG
3480
+ });
3457
3481
  const merged = { ...settings, hooks: claudeHooks };
3458
3482
  const fileContent = JSON.stringify(merged, null, 2);
3459
3483
  return new _ClaudecodeHooks({
@@ -3476,7 +3500,10 @@ var ClaudecodeHooks = class _ClaudecodeHooks extends ToolHooks {
3476
3500
  }
3477
3501
  );
3478
3502
  }
3479
- const hooks = claudeHooksToCanonical(settings.hooks);
3503
+ const hooks = toolHooksToCanonical({
3504
+ hooks: settings.hooks,
3505
+ converterConfig: CLAUDE_CONVERTER_CONFIG
3506
+ });
3480
3507
  return this.toRulesyncHooksDefault({
3481
3508
  fileContent: JSON.stringify({ version: 1, hooks }, null, 2)
3482
3509
  });
@@ -3785,91 +3812,12 @@ var CursorHooks = class _CursorHooks extends ToolHooks {
3785
3812
 
3786
3813
  // src/features/hooks/factorydroid-hooks.ts
3787
3814
  var import_node_path25 = require("path");
3788
- function canonicalToFactorydroidHooks(config) {
3789
- const supported = new Set(FACTORYDROID_HOOK_EVENTS);
3790
- const sharedHooks = {};
3791
- for (const [event, defs] of Object.entries(config.hooks)) {
3792
- if (supported.has(event)) {
3793
- sharedHooks[event] = defs;
3794
- }
3795
- }
3796
- const effectiveHooks = {
3797
- ...sharedHooks,
3798
- ...config.factorydroid?.hooks
3799
- };
3800
- const result = {};
3801
- for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
3802
- const pascalEventName = CANONICAL_TO_FACTORYDROID_EVENT_NAMES[eventName] ?? eventName;
3803
- const byMatcher = /* @__PURE__ */ new Map();
3804
- for (const def of definitions) {
3805
- const key = def.matcher ?? "";
3806
- const list = byMatcher.get(key);
3807
- if (list) list.push(def);
3808
- else byMatcher.set(key, [def]);
3809
- }
3810
- const entries = [];
3811
- for (const [matcherKey, defs] of byMatcher) {
3812
- const hooks = defs.map((def) => {
3813
- const command = def.command !== void 0 && def.command !== null && !def.command.startsWith("$") ? `$FACTORY_PROJECT_DIR/${def.command.replace(/^\.\//, "")}` : def.command;
3814
- return {
3815
- type: def.type ?? "command",
3816
- ...command !== void 0 && command !== null && { command },
3817
- ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
3818
- ...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt }
3819
- };
3820
- });
3821
- entries.push(matcherKey ? { matcher: matcherKey, hooks } : { hooks });
3822
- }
3823
- result[pascalEventName] = entries;
3824
- }
3825
- return result;
3826
- }
3827
- function isFactorydroidMatcherEntry(x) {
3828
- if (x === null || typeof x !== "object") {
3829
- return false;
3830
- }
3831
- if ("matcher" in x && typeof x.matcher !== "string") {
3832
- return false;
3833
- }
3834
- if ("hooks" in x && !Array.isArray(x.hooks)) {
3835
- return false;
3836
- }
3837
- return true;
3838
- }
3839
- function factorydroidHooksToCanonical(hooks) {
3840
- if (hooks === null || hooks === void 0 || typeof hooks !== "object") {
3841
- return {};
3842
- }
3843
- const canonical = {};
3844
- for (const [pascalEventName, matcherEntries] of Object.entries(hooks)) {
3845
- const eventName = FACTORYDROID_TO_CANONICAL_EVENT_NAMES[pascalEventName] ?? pascalEventName;
3846
- if (!Array.isArray(matcherEntries)) continue;
3847
- const defs = [];
3848
- for (const rawEntry of matcherEntries) {
3849
- if (!isFactorydroidMatcherEntry(rawEntry)) continue;
3850
- const entry = rawEntry;
3851
- const hookDefs = entry.hooks ?? [];
3852
- for (const h of hookDefs) {
3853
- const cmd = typeof h.command === "string" ? h.command : void 0;
3854
- const command = typeof cmd === "string" && cmd.includes("$FACTORY_PROJECT_DIR/") ? cmd.replace(/^\$FACTORY_PROJECT_DIR\/?/, "./") : cmd;
3855
- const hookType = h.type === "command" || h.type === "prompt" ? h.type : "command";
3856
- const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
3857
- const prompt = typeof h.prompt === "string" ? h.prompt : void 0;
3858
- defs.push({
3859
- type: hookType,
3860
- ...command !== void 0 && command !== null && { command },
3861
- ...timeout !== void 0 && timeout !== null && { timeout },
3862
- ...prompt !== void 0 && prompt !== null && { prompt },
3863
- ...entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && { matcher: entry.matcher }
3864
- });
3865
- }
3866
- }
3867
- if (defs.length > 0) {
3868
- canonical[eventName] = defs;
3869
- }
3870
- }
3871
- return canonical;
3872
- }
3815
+ var FACTORYDROID_CONVERTER_CONFIG = {
3816
+ supportedEvents: FACTORYDROID_HOOK_EVENTS,
3817
+ canonicalToToolEventNames: CANONICAL_TO_FACTORYDROID_EVENT_NAMES,
3818
+ toolToCanonicalEventNames: FACTORYDROID_TO_CANONICAL_EVENT_NAMES,
3819
+ projectDirVar: "$FACTORY_PROJECT_DIR"
3820
+ };
3873
3821
  var FactorydroidHooks = class _FactorydroidHooks extends ToolHooks {
3874
3822
  constructor(params) {
3875
3823
  super({
@@ -3921,7 +3869,11 @@ var FactorydroidHooks = class _FactorydroidHooks extends ToolHooks {
3921
3869
  );
3922
3870
  }
3923
3871
  const config = rulesyncHooks.getJson();
3924
- const factorydroidHooks = canonicalToFactorydroidHooks(config);
3872
+ const factorydroidHooks = canonicalToToolHooks({
3873
+ config,
3874
+ toolOverrideHooks: config.factorydroid?.hooks,
3875
+ converterConfig: FACTORYDROID_CONVERTER_CONFIG
3876
+ });
3925
3877
  const merged = { ...settings, hooks: factorydroidHooks };
3926
3878
  const fileContent = JSON.stringify(merged, null, 2);
3927
3879
  return new _FactorydroidHooks({
@@ -3944,7 +3896,10 @@ var FactorydroidHooks = class _FactorydroidHooks extends ToolHooks {
3944
3896
  }
3945
3897
  );
3946
3898
  }
3947
- const hooks = factorydroidHooksToCanonical(settings.hooks);
3899
+ const hooks = toolHooksToCanonical({
3900
+ hooks: settings.hooks,
3901
+ converterConfig: FACTORYDROID_CONVERTER_CONFIG
3902
+ });
3948
3903
  return this.toRulesyncHooksDefault({
3949
3904
  fileContent: JSON.stringify({ version: 1, hooks }, null, 2)
3950
3905
  });
@@ -3995,7 +3950,10 @@ function canonicalToGeminicliHooks(config) {
3995
3950
  const entries = [];
3996
3951
  for (const [matcherKey, defs] of byMatcher) {
3997
3952
  const hooks = defs.map((def) => {
3998
- const command = def.command !== void 0 && def.command !== null && !def.command.startsWith("$") ? `$GEMINI_PROJECT_DIR/${def.command.replace(/^\.\//, "")}` : def.command;
3953
+ const commandText = def.command;
3954
+ const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
3955
+ const shouldPrefix = typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && trimmedCommand.startsWith(".");
3956
+ const command = shouldPrefix && typeof trimmedCommand === "string" ? `$GEMINI_PROJECT_DIR/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
3999
3957
  return {
4000
3958
  type: def.type ?? "command",
4001
3959
  ...command !== void 0 && command !== null && { command },
@@ -10741,7 +10699,7 @@ var ToolSubagent = class extends ToolFile {
10741
10699
  // src/features/subagents/simulated-subagent.ts
10742
10700
  var SimulatedSubagentFrontmatterSchema = import_mini39.z.object({
10743
10701
  name: import_mini39.z.string(),
10744
- description: import_mini39.z.string()
10702
+ description: import_mini39.z.optional(import_mini39.z.string())
10745
10703
  });
10746
10704
  var SimulatedSubagent = class extends ToolSubagent {
10747
10705
  frontmatter;
@@ -10965,7 +10923,7 @@ var import_mini40 = require("zod/mini");
10965
10923
  var RulesyncSubagentFrontmatterSchema = import_mini40.z.looseObject({
10966
10924
  targets: import_mini40.z._default(RulesyncTargetsSchema, ["*"]),
10967
10925
  name: import_mini40.z.string(),
10968
- description: import_mini40.z.string()
10926
+ description: import_mini40.z.optional(import_mini40.z.string())
10969
10927
  });
10970
10928
  var RulesyncSubagent = class _RulesyncSubagent extends RulesyncFile {
10971
10929
  frontmatter;
@@ -11036,7 +10994,7 @@ var RulesyncSubagent = class _RulesyncSubagent extends RulesyncFile {
11036
10994
  // src/features/subagents/claudecode-subagent.ts
11037
10995
  var ClaudecodeSubagentFrontmatterSchema = import_mini41.z.looseObject({
11038
10996
  name: import_mini41.z.string(),
11039
- description: import_mini41.z.string(),
10997
+ description: import_mini41.z.optional(import_mini41.z.string()),
11040
10998
  model: import_mini41.z.optional(import_mini41.z.string()),
11041
10999
  tools: import_mini41.z.optional(import_mini41.z.union([import_mini41.z.string(), import_mini41.z.array(import_mini41.z.string())])),
11042
11000
  permissionMode: import_mini41.z.optional(import_mini41.z.string()),
@@ -11246,7 +11204,7 @@ var CodexCliSubagent = class _CodexCliSubagent extends ToolSubagent {
11246
11204
  const rulesyncFrontmatter = {
11247
11205
  targets: ["codexcli"],
11248
11206
  name,
11249
- description: description ?? "",
11207
+ description,
11250
11208
  // Only include codexcli section if there are fields
11251
11209
  ...Object.keys(codexcliSection).length > 0 && { codexcli: codexcliSection }
11252
11210
  };
@@ -11359,7 +11317,7 @@ var import_mini43 = require("zod/mini");
11359
11317
  var REQUIRED_TOOL = "agent/runSubagent";
11360
11318
  var CopilotSubagentFrontmatterSchema = import_mini43.z.looseObject({
11361
11319
  name: import_mini43.z.string(),
11362
- description: import_mini43.z.string(),
11320
+ description: import_mini43.z.optional(import_mini43.z.string()),
11363
11321
  tools: import_mini43.z.optional(import_mini43.z.union([import_mini43.z.string(), import_mini43.z.array(import_mini43.z.string())]))
11364
11322
  });
11365
11323
  var normalizeTools = (tools) => {
@@ -11524,7 +11482,7 @@ var import_node_path86 = require("path");
11524
11482
  var import_mini44 = require("zod/mini");
11525
11483
  var CursorSubagentFrontmatterSchema = import_mini44.z.looseObject({
11526
11484
  name: import_mini44.z.string(),
11527
- description: import_mini44.z.string()
11485
+ description: import_mini44.z.optional(import_mini44.z.string())
11528
11486
  });
11529
11487
  var CursorSubagent = class _CursorSubagent extends ToolSubagent {
11530
11488
  frontmatter;
@@ -11729,7 +11687,7 @@ var KiroSubagent = class _KiroSubagent extends ToolSubagent {
11729
11687
  const rulesyncFrontmatter = {
11730
11688
  targets: ["kiro"],
11731
11689
  name,
11732
- description: description ?? "",
11690
+ description: description ?? void 0,
11733
11691
  // Only include kiro section if there are fields
11734
11692
  ...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
11735
11693
  };
@@ -11840,7 +11798,7 @@ var KiroSubagent = class _KiroSubagent extends ToolSubagent {
11840
11798
  var import_node_path88 = require("path");
11841
11799
  var import_mini46 = require("zod/mini");
11842
11800
  var OpenCodeSubagentFrontmatterSchema = import_mini46.z.looseObject({
11843
- description: import_mini46.z.string(),
11801
+ description: import_mini46.z.optional(import_mini46.z.string()),
11844
11802
  mode: import_mini46.z._default(import_mini46.z.string(), "subagent"),
11845
11803
  name: import_mini46.z.optional(import_mini46.z.string())
11846
11804
  });
@@ -12670,7 +12628,7 @@ var globStrategy = {
12670
12628
  },
12671
12629
  exportRulesyncData: ({ description, ...frontmatter }) => ({
12672
12630
  globs: parseGlobsString(frontmatter.globs),
12673
- description: description || "",
12631
+ description,
12674
12632
  antigravity: frontmatter
12675
12633
  })
12676
12634
  };
@@ -12682,7 +12640,7 @@ var manualStrategy = {
12682
12640
  }),
12683
12641
  exportRulesyncData: ({ description, ...frontmatter }) => ({
12684
12642
  globs: [],
12685
- description: description || "",
12643
+ description,
12686
12644
  antigravity: frontmatter
12687
12645
  })
12688
12646
  };
@@ -12694,7 +12652,7 @@ var alwaysOnStrategy = {
12694
12652
  }),
12695
12653
  exportRulesyncData: ({ description, ...frontmatter }) => ({
12696
12654
  globs: ["**/*"],
12697
- description: description || "",
12655
+ description,
12698
12656
  antigravity: frontmatter
12699
12657
  })
12700
12658
  };
@@ -12707,7 +12665,7 @@ var modelDecisionStrategy = {
12707
12665
  }),
12708
12666
  exportRulesyncData: ({ description, ...frontmatter }) => ({
12709
12667
  globs: [],
12710
- description: description || "",
12668
+ description,
12711
12669
  antigravity: frontmatter
12712
12670
  })
12713
12671
  };
@@ -12722,7 +12680,7 @@ var unknownStrategy = {
12722
12680
  },
12723
12681
  exportRulesyncData: ({ description, ...frontmatter }) => ({
12724
12682
  globs: frontmatter.globs ? parseGlobsString(frontmatter.globs) : ["**/*"],
12725
- description: description || "",
12683
+ description,
12726
12684
  antigravity: frontmatter
12727
12685
  })
12728
12686
  };
@@ -12744,7 +12702,7 @@ var inferenceStrategy = {
12744
12702
  },
12745
12703
  exportRulesyncData: ({ description, ...frontmatter }) => ({
12746
12704
  globs: frontmatter.globs ? parseGlobsString(frontmatter.globs) : ["**/*"],
12747
- description: description || "",
12705
+ description,
12748
12706
  antigravity: frontmatter
12749
12707
  })
12750
12708
  };
@@ -12874,7 +12832,6 @@ var AntigravityRule = class _AntigravityRule extends ToolRule {
12874
12832
  const strategy = STRATEGIES.find((s) => s.canHandle(this.frontmatter.trigger));
12875
12833
  let rulesyncData = {
12876
12834
  globs: [],
12877
- description: "",
12878
12835
  antigravity: this.frontmatter
12879
12836
  };
12880
12837
  if (strategy) {
@@ -12944,7 +12901,6 @@ var AugmentcodeLegacyRule = class _AugmentcodeLegacyRule extends ToolRule {
12944
12901
  const rulesyncFrontmatter = {
12945
12902
  root: this.isRoot(),
12946
12903
  targets: ["*"],
12947
- description: "",
12948
12904
  globs: this.isRoot() ? ["**/*"] : []
12949
12905
  };
12950
12906
  return new RulesyncRule({
@@ -13119,6 +13075,12 @@ var ClaudecodeLegacyRule = class _ClaudecodeLegacyRule extends ToolRule {
13119
13075
  relativeDirPath: ".",
13120
13076
  relativeFilePath: "CLAUDE.md"
13121
13077
  },
13078
+ alternativeRoots: [
13079
+ {
13080
+ relativeDirPath: ".claude",
13081
+ relativeFilePath: "CLAUDE.md"
13082
+ }
13083
+ ],
13122
13084
  nonRoot: {
13123
13085
  relativeDirPath: buildToolPath(".claude", "memories", excludeToolDir)
13124
13086
  }
@@ -13128,18 +13090,19 @@ var ClaudecodeLegacyRule = class _ClaudecodeLegacyRule extends ToolRule {
13128
13090
  baseDir = process.cwd(),
13129
13091
  relativeFilePath,
13130
13092
  validate = true,
13131
- global = false
13093
+ global = false,
13094
+ relativeDirPath: overrideDirPath
13132
13095
  }) {
13133
13096
  const paths = this.getSettablePaths({ global });
13134
13097
  const isRoot = relativeFilePath === paths.root.relativeFilePath;
13135
13098
  if (isRoot) {
13136
- const relativePath2 = paths.root.relativeFilePath;
13099
+ const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
13137
13100
  const fileContent2 = await readFileContent(
13138
- (0, import_node_path96.join)(baseDir, paths.root.relativeDirPath, relativePath2)
13101
+ (0, import_node_path96.join)(baseDir, rootDirPath, paths.root.relativeFilePath)
13139
13102
  );
13140
13103
  return new _ClaudecodeLegacyRule({
13141
13104
  baseDir,
13142
- relativeDirPath: paths.root.relativeDirPath,
13105
+ relativeDirPath: rootDirPath,
13143
13106
  relativeFilePath: paths.root.relativeFilePath,
13144
13107
  fileContent: fileContent2,
13145
13108
  validate,
@@ -13234,6 +13197,12 @@ var ClaudecodeRule = class _ClaudecodeRule extends ToolRule {
13234
13197
  relativeDirPath: ".",
13235
13198
  relativeFilePath: "CLAUDE.md"
13236
13199
  },
13200
+ alternativeRoots: [
13201
+ {
13202
+ relativeDirPath: ".claude",
13203
+ relativeFilePath: "CLAUDE.md"
13204
+ }
13205
+ ],
13237
13206
  nonRoot: {
13238
13207
  relativeDirPath: buildToolPath(".claude", "rules", excludeToolDir)
13239
13208
  }
@@ -13266,17 +13235,19 @@ var ClaudecodeRule = class _ClaudecodeRule extends ToolRule {
13266
13235
  baseDir = process.cwd(),
13267
13236
  relativeFilePath,
13268
13237
  validate = true,
13269
- global = false
13238
+ global = false,
13239
+ relativeDirPath: overrideDirPath
13270
13240
  }) {
13271
13241
  const paths = this.getSettablePaths({ global });
13272
13242
  const isRoot = relativeFilePath === paths.root.relativeFilePath;
13273
13243
  if (isRoot) {
13244
+ const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
13274
13245
  const fileContent2 = await readFileContent(
13275
- (0, import_node_path97.join)(baseDir, paths.root.relativeDirPath, paths.root.relativeFilePath)
13246
+ (0, import_node_path97.join)(baseDir, rootDirPath, paths.root.relativeFilePath)
13276
13247
  );
13277
13248
  return new _ClaudecodeRule({
13278
13249
  baseDir,
13279
- relativeDirPath: paths.root.relativeDirPath,
13250
+ relativeDirPath: rootDirPath,
13280
13251
  relativeFilePath: paths.root.relativeFilePath,
13281
13252
  frontmatter: {},
13282
13253
  body: fileContent2.trim(),
@@ -15633,35 +15604,62 @@ var RulesProcessor = class extends FeatureProcessor {
15633
15604
  const settablePaths = factory.class.getSettablePaths({
15634
15605
  global: this.global
15635
15606
  });
15607
+ const resolveRelativeDirPath = (filePath) => {
15608
+ const dirName = (0, import_node_path114.dirname)((0, import_node_path114.relative)(this.baseDir, filePath));
15609
+ return dirName === "" ? "." : dirName;
15610
+ };
15611
+ const findFilesWithFallback = async (primaryGlob, alternativeRoots, buildAltGlob) => {
15612
+ const primaryFilePaths = await findFilesByGlobs(primaryGlob);
15613
+ if (primaryFilePaths.length > 0) {
15614
+ return primaryFilePaths;
15615
+ }
15616
+ if (alternativeRoots) {
15617
+ return findFilesByGlobs(alternativeRoots.map(buildAltGlob));
15618
+ }
15619
+ return [];
15620
+ };
15636
15621
  const rootToolRules = await (async () => {
15637
15622
  if (!settablePaths.root) {
15638
15623
  return [];
15639
15624
  }
15640
- const rootFilePaths = await findFilesByGlobs(
15625
+ const uniqueRootFilePaths = await findFilesWithFallback(
15641
15626
  (0, import_node_path114.join)(
15642
15627
  this.baseDir,
15643
15628
  settablePaths.root.relativeDirPath ?? ".",
15644
15629
  settablePaths.root.relativeFilePath
15645
- )
15630
+ ),
15631
+ settablePaths.alternativeRoots,
15632
+ (alt) => (0, import_node_path114.join)(this.baseDir, alt.relativeDirPath, alt.relativeFilePath)
15646
15633
  );
15647
15634
  if (forDeletion) {
15648
- return rootFilePaths.map(
15649
- (filePath) => factory.class.forDeletion({
15635
+ return uniqueRootFilePaths.map((filePath) => {
15636
+ const relativeDirPath = resolveRelativeDirPath(filePath);
15637
+ checkPathTraversal({
15638
+ relativePath: relativeDirPath,
15639
+ intendedRootDir: this.baseDir
15640
+ });
15641
+ return factory.class.forDeletion({
15650
15642
  baseDir: this.baseDir,
15651
- relativeDirPath: settablePaths.root?.relativeDirPath ?? ".",
15643
+ relativeDirPath,
15652
15644
  relativeFilePath: (0, import_node_path114.basename)(filePath),
15653
15645
  global: this.global
15654
- })
15655
- ).filter((rule) => rule.isDeletable());
15646
+ });
15647
+ }).filter((rule) => rule.isDeletable());
15656
15648
  }
15657
15649
  return await Promise.all(
15658
- rootFilePaths.map(
15659
- (filePath) => factory.class.fromFile({
15650
+ uniqueRootFilePaths.map((filePath) => {
15651
+ const relativeDirPath = resolveRelativeDirPath(filePath);
15652
+ checkPathTraversal({
15653
+ relativePath: relativeDirPath,
15654
+ intendedRootDir: this.baseDir
15655
+ });
15656
+ return factory.class.fromFile({
15660
15657
  baseDir: this.baseDir,
15661
15658
  relativeFilePath: (0, import_node_path114.basename)(filePath),
15659
+ relativeDirPath,
15662
15660
  global: this.global
15663
- })
15664
- )
15661
+ });
15662
+ })
15665
15663
  );
15666
15664
  })();
15667
15665
  logger.debug(`Found ${rootToolRules.length} root tool rule files`);
@@ -15675,17 +15673,24 @@ var RulesProcessor = class extends FeatureProcessor {
15675
15673
  if (!settablePaths.root) {
15676
15674
  return [];
15677
15675
  }
15678
- const localRootFilePaths = await findFilesByGlobs(
15679
- (0, import_node_path114.join)(this.baseDir, settablePaths.root.relativeDirPath ?? ".", "CLAUDE.local.md")
15676
+ const uniqueLocalRootFilePaths = await findFilesWithFallback(
15677
+ (0, import_node_path114.join)(this.baseDir, settablePaths.root.relativeDirPath ?? ".", "CLAUDE.local.md"),
15678
+ settablePaths.alternativeRoots,
15679
+ (alt) => (0, import_node_path114.join)(this.baseDir, alt.relativeDirPath, "CLAUDE.local.md")
15680
15680
  );
15681
- return localRootFilePaths.map(
15682
- (filePath) => factory.class.forDeletion({
15681
+ return uniqueLocalRootFilePaths.map((filePath) => {
15682
+ const relativeDirPath = resolveRelativeDirPath(filePath);
15683
+ checkPathTraversal({
15684
+ relativePath: relativeDirPath,
15685
+ intendedRootDir: this.baseDir
15686
+ });
15687
+ return factory.class.forDeletion({
15683
15688
  baseDir: this.baseDir,
15684
- relativeDirPath: settablePaths.root?.relativeDirPath ?? ".",
15689
+ relativeDirPath,
15685
15690
  relativeFilePath: (0, import_node_path114.basename)(filePath),
15686
15691
  global: this.global
15687
- })
15688
- ).filter((rule) => rule.isDeletable());
15692
+ });
15693
+ }).filter((rule) => rule.isDeletable());
15689
15694
  })();
15690
15695
  logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
15691
15696
  const nonRootToolRules = await (async () => {