rulesync 16.36.0 → 16.37.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.
@@ -8420,6 +8420,7 @@ const RulesyncRuleFrontmatterSchema = z.object({
8420
8420
  trigger: z.optional(z.string()),
8421
8421
  globs: z.optional(z.array(z.string()))
8422
8422
  })),
8423
+ aiassistant: z.optional(z.looseObject({ apply: z.optional(z.string()) })),
8423
8424
  devin: z.optional(z.looseObject({
8424
8425
  trigger: z.optional(z.string()),
8425
8426
  globs: z.optional(z.array(z.string())),
@@ -9644,6 +9645,8 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
9644
9645
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
9645
9646
  "disable-model-invocation": z.optional(z.boolean()),
9646
9647
  "user-invocable": z.optional(z.boolean()),
9648
+ icon: z.optional(z.string()),
9649
+ color: z.optional(z.string()),
9647
9650
  metadata: z.optional(z.looseObject({}))
9648
9651
  })),
9649
9652
  factorydroid: z.optional(z.looseObject({
@@ -23658,14 +23661,23 @@ var ContinueHooks = class extends SettingsJsonHooks {
23658
23661
  * portable `command` field is written, which upstream copies to both when
23659
23662
  * neither is present. Note the cloud agent runs hooks in a Linux sandbox and
23660
23663
  * honors only `bash` and `command` — a `powershell` entry is ignored there.
23664
+ * VS Code reads the same file and maps `bash` to `osx` / `linux` and
23665
+ * `powershell` to `windows`; it also documents its own per-OS overrides
23666
+ * (`windows`, `linux`, `osx`) next to `command`, which are typed here so an
23667
+ * entry that uses them instead of a shell field is not mistaken for one
23668
+ * without a command.
23661
23669
  *
23662
23670
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
23671
+ * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
23663
23672
  */
23664
23673
  const CopilotHookEntrySchema = z.looseObject({
23665
23674
  type: z.string(),
23666
23675
  bash: z.optional(z.string()),
23667
23676
  powershell: z.optional(z.string()),
23668
23677
  command: z.optional(z.string()),
23678
+ windows: z.optional(z.string()),
23679
+ linux: z.optional(z.string()),
23680
+ osx: z.optional(z.string()),
23669
23681
  cwd: z.optional(z.string()),
23670
23682
  env: z.optional(z.record(z.string(), z.string())),
23671
23683
  timeoutSec: z.optional(z.number()),
@@ -23756,17 +23768,30 @@ function resolveImportCommand$1(entry, logger) {
23756
23768
  *
23757
23769
  * Generate re-emits any non-canonical key verbatim through `rest`, so a key
23758
23770
  * dropped here does not survive an import → generate round trip. `cwd` is a
23759
- * documented Copilot hook field and was previously lost that way.
23771
+ * documented Copilot hook field and was previously lost that way, as were the
23772
+ * VS Code per-OS overrides `windows` / `linux` / `osx`: an entry authored with
23773
+ * only those (no `command`) imported as a hook with no command at all and was
23774
+ * regenerated empty.
23760
23775
  *
23761
23776
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
23762
23777
  * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
23778
+ * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
23763
23779
  */
23764
23780
  function importPassthrough$1(entry) {
23765
23781
  const passthrough = {};
23766
23782
  if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
23767
23783
  if (entry.env !== void 0) passthrough.env = entry.env;
23784
+ for (const key of OS_OVERRIDE_KEYS) if (entry[key] !== void 0) passthrough[key] = entry[key];
23768
23785
  return passthrough;
23769
23786
  }
23787
+ const OS_OVERRIDE_KEYS = [
23788
+ "windows",
23789
+ "linux",
23790
+ "osx"
23791
+ ];
23792
+ function hasOsOverride(passthrough) {
23793
+ return OS_OVERRIDE_KEYS.some((key) => passthrough[key] !== void 0);
23794
+ }
23770
23795
  /**
23771
23796
  * Extract hooks from Copilot hooks JSON into canonical format.
23772
23797
  * Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
@@ -23787,12 +23812,14 @@ function copilotHooksToCanonical(copilotHooks, logger) {
23787
23812
  const entry = parseResult.data;
23788
23813
  const { command, shell } = resolveImportCommand$1(entry, logger);
23789
23814
  const timeout = entry.timeoutSec ?? entry.timeout;
23815
+ const passthrough = importPassthrough$1(entry);
23816
+ if (command === void 0 && hasOsOverride(passthrough)) logger?.warn(`Copilot hook on '${copilotEventName}' has only VS Code per-OS overrides (windows/linux/osx) and no portable command; it is preserved for copilot, but other targets will generate it without a command until a \`command\` is added.`);
23790
23817
  defs.push({
23791
23818
  type: "command",
23792
23819
  ...command !== void 0 && { command },
23793
23820
  ...shell !== void 0 && { shell },
23794
23821
  ...timeout !== void 0 && { timeout },
23795
- ...importPassthrough$1(entry)
23822
+ ...passthrough
23796
23823
  });
23797
23824
  }
23798
23825
  if (defs.length > 0) canonical[eventName] = defs;
@@ -23955,6 +23982,8 @@ const CopilotCliHookEntrySchema = z.looseObject({
23955
23982
  bash: z.optional(z.string()),
23956
23983
  powershell: z.optional(z.string()),
23957
23984
  command: z.optional(z.string()),
23985
+ exec: z.optional(z.string()),
23986
+ args: z.optional(z.array(z.string())),
23958
23987
  prompt: z.optional(z.string()),
23959
23988
  url: z.optional(z.string()),
23960
23989
  headers: z.optional(z.record(z.string(), z.string())),
@@ -23984,6 +24013,32 @@ function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger
23984
24013
  return {};
23985
24014
  }
23986
24015
  /**
24016
+ * Pick the command fields of an exported `command` entry.
24017
+ *
24018
+ * A canonical `args` list (an empty one counts) selects the CLI's shell-free
24019
+ * form: `command` becomes `exec` and the list is written as `args`, so the
24020
+ * executable is spawned directly. The `shell` selector is meaningless there
24021
+ * and is dropped with a warning. Otherwise the canonical `shell` selector
24022
+ * picks the shell-specific field; without it the portable `command` field is
24023
+ * written, which upstream copies to both `bash` and `powershell`. Keying off
24024
+ * `shell` rather than `process.platform` keeps the generated file identical
24025
+ * on every machine.
24026
+ */
24027
+ function resolveExportCommandPart({ def, eventName, logger }) {
24028
+ if (Array.isArray(def.args)) {
24029
+ if (def.shell !== void 0) logger?.warn(`Copilot CLI runs an \`exec\` hook without a shell; dropping shell "${def.shell}" on '${eventName}'.`);
24030
+ return compact({
24031
+ exec: def.command,
24032
+ args: def.args,
24033
+ env: def.env
24034
+ });
24035
+ }
24036
+ return compact({
24037
+ [def.shell ?? "command"]: def.command,
24038
+ env: def.env
24039
+ });
24040
+ }
24041
+ /**
23987
24042
  * Build the exported entries for a single canonical event. Returns an empty
23988
24043
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
23989
24044
  */
@@ -24023,19 +24078,17 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
24023
24078
  ...timeoutPart,
24024
24079
  ...rest
24025
24080
  });
24026
- else if (hookType === "command") {
24027
- const commandField = def.shell ?? "command";
24028
- entries.push({
24029
- type: "command",
24030
- ...matcherPart,
24031
- ...compact({
24032
- [commandField]: def.command,
24033
- env: def.env
24034
- }),
24035
- ...timeoutPart,
24036
- ...rest
24037
- });
24038
- }
24081
+ else if (hookType === "command") entries.push({
24082
+ type: "command",
24083
+ ...matcherPart,
24084
+ ...resolveExportCommandPart({
24085
+ def,
24086
+ eventName,
24087
+ logger
24088
+ }),
24089
+ ...timeoutPart,
24090
+ ...rest
24091
+ });
24039
24092
  }
24040
24093
  return entries;
24041
24094
  }
@@ -24075,6 +24128,10 @@ function importPassthrough(entry) {
24075
24128
  /**
24076
24129
  * Resolve the canonical command and its `shell` selector from an imported entry.
24077
24130
  *
24131
+ * The CLI-only `exec` form maps to canonical `command` + `args`, the exec form
24132
+ * shared with AugmentCode and Claude Code, so it no longer imports as a hook
24133
+ * with no command at all.
24134
+ *
24078
24135
  * A shell-specific field carries its `shell` through so re-export writes the
24079
24136
  * same field back. An entry using only the portable `command` field leaves
24080
24137
  * `shell` unset, which re-export renders as the portable field again.
@@ -24088,6 +24145,13 @@ function importPassthrough(entry) {
24088
24145
  function resolveImportCommand(entry, logger) {
24089
24146
  const hasBash = typeof entry.bash === "string";
24090
24147
  const hasPowershell = typeof entry.powershell === "string";
24148
+ if (typeof entry.exec === "string") {
24149
+ if (hasBash || hasPowershell || typeof entry.command === "string") logger?.warn("Copilot CLI hook has both exec and a shell command; using exec and ignoring the shell fields, as the hooks reference says not to combine them.");
24150
+ return {
24151
+ command: entry.exec,
24152
+ args: entry.args ?? []
24153
+ };
24154
+ }
24091
24155
  if (hasBash && hasPowershell) {
24092
24156
  logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
24093
24157
  return {
@@ -24137,11 +24201,12 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
24137
24201
  ...passthrough
24138
24202
  });
24139
24203
  else {
24140
- const { command, shell } = resolveImportCommand(entry, logger);
24204
+ const { command, shell, args } = resolveImportCommand(entry, logger);
24141
24205
  defs.push({
24142
24206
  type: "command",
24143
24207
  ...command !== void 0 && { command },
24144
24208
  ...shell !== void 0 && { shell },
24209
+ ...args !== void 0 && { args },
24145
24210
  ...matcherPart,
24146
24211
  ...timeoutPart,
24147
24212
  ...passthrough
@@ -24691,6 +24756,7 @@ var CursorHooks = class CursorHooks extends ToolHooks {
24691
24756
  ...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
24692
24757
  ...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher },
24693
24758
  ...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt },
24759
+ ...def.type === "prompt" && def.model !== void 0 && def.model !== null && { model: def.model },
24694
24760
  ...def.failClosed !== void 0 && def.failClosed !== null && { failClosed: def.failClosed }
24695
24761
  }));
24696
24762
  if (mappedDefs.length > 0) mappedHooks[cursorEventName] = mappedDefs;
@@ -59175,8 +59241,11 @@ const CursorSkillFrontmatterSchema = z.looseObject({
59175
59241
  name: z.string(),
59176
59242
  description: z.string(),
59177
59243
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
59244
+ globs: z.optional(z.union([z.string(), z.array(z.string())])),
59178
59245
  "disable-model-invocation": z.optional(z.boolean()),
59179
59246
  "user-invocable": z.optional(z.boolean()),
59247
+ icon: z.optional(z.string()),
59248
+ color: z.optional(z.string()),
59180
59249
  metadata: z.optional(z.looseObject({}))
59181
59250
  });
59182
59251
  /**
@@ -59228,10 +59297,13 @@ var CursorSkill = class CursorSkill extends ToolSkill {
59228
59297
  }
59229
59298
  toRulesyncSkill() {
59230
59299
  const frontmatter = this.getFrontmatter();
59300
+ const paths = frontmatter.paths ?? frontmatter.globs;
59231
59301
  const cursorSection = {
59232
- ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
59302
+ ...paths !== void 0 && { paths },
59233
59303
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
59234
59304
  ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
59305
+ ...frontmatter.icon !== void 0 && { icon: frontmatter.icon },
59306
+ ...frontmatter.color !== void 0 && { color: frontmatter.color },
59235
59307
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
59236
59308
  };
59237
59309
  const rulesyncFrontmatter = {
@@ -59273,6 +59345,8 @@ var CursorSkill = class CursorSkill extends ToolSkill {
59273
59345
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
59274
59346
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
59275
59347
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
59348
+ ...cursorSection?.icon !== void 0 && { icon: cursorSection.icon },
59349
+ ...cursorSection?.color !== void 0 && { color: cursorSection.color },
59276
59350
  ...metadata !== void 0 && { metadata }
59277
59351
  };
59278
59352
  return new CursorSkill({
@@ -66032,11 +66106,17 @@ var CopilotSubagent = class CopilotSubagent extends ToolSubagent {
66032
66106
  *
66033
66107
  * Reference: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli
66034
66108
  * https://docs.github.com/en/copilot/reference/custom-agents-configuration
66109
+ * https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference
66035
66110
  *
66036
66111
  * Per the configuration docs:
66037
66112
  * - `description` is required.
66038
66113
  * - `name`, `target`, `tools`, `model`, `disable-model-invocation`,
66039
66114
  * `user-invocable`, `mcp-servers`, and `metadata` are optional.
66115
+ * - The CLI command reference adds `models` (authored models in priority
66116
+ * order, overriding `model` when both are set) and `modelPolicy`
66117
+ * (`preferred` lets a `subagents` settings override or the `/subagents`
66118
+ * picker replace the model; `required` locks dispatch to the authored
66119
+ * list). CLI v1.0.81+; the shared reference page still lists `model` only.
66040
66120
  *
66041
66121
  * `looseObject` is used so that future additional fields are passed through
66042
66122
  * unchanged (consistent with the rest of rulesync's frontmatter schemas).
@@ -66047,6 +66127,8 @@ const CopilotCliSubagentFrontmatterSchema = z.looseObject({
66047
66127
  target: z.optional(z.string()),
66048
66128
  tools: z.optional(z.union([z.string(), z.array(z.string())])),
66049
66129
  model: z.optional(z.string()),
66130
+ models: z.optional(z.array(z.string())),
66131
+ modelPolicy: z.optional(z.enum(["preferred", "required"])),
66050
66132
  "disable-model-invocation": z.optional(z.boolean()),
66051
66133
  "user-invocable": z.optional(z.boolean()),
66052
66134
  "mcp-servers": z.optional(z.record(z.string(), z.unknown())),
@@ -70423,16 +70505,131 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
70423
70505
  }
70424
70506
  };
70425
70507
  //#endregion
70508
+ //#region src/utils/brace-aware-list.ts
70509
+ /**
70510
+ * Splits a comma-separated pattern list on the commas that separate its
70511
+ * entries, leaving the commas inside a brace group alone: in
70512
+ * `{src,lib}/**\/*.ts, tests/**\/*.test.ts` the first comma belongs to the
70513
+ * brace expansion and the second separates the two globs. Entries are trimmed;
70514
+ * empty entries are dropped.
70515
+ *
70516
+ * @example
70517
+ * splitBraceAwareList("src/**\/*.{ts,tsx}, docs/**")
70518
+ * // => ["src/**\/*.{ts,tsx}", "docs/**"]
70519
+ */
70520
+ const splitBraceAwareList = (value) => {
70521
+ const entries = [];
70522
+ let current = "";
70523
+ let braceDepth = 0;
70524
+ for (const char of value) {
70525
+ if (char === "{") braceDepth += 1;
70526
+ else if (char === "}" && braceDepth > 0) braceDepth -= 1;
70527
+ else if (char === "," && braceDepth === 0) {
70528
+ entries.push(current);
70529
+ current = "";
70530
+ continue;
70531
+ }
70532
+ current += char;
70533
+ }
70534
+ entries.push(current);
70535
+ return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
70536
+ };
70537
+ /**
70538
+ * Upper bound on the patterns one glob may expand to; past it the glob is
70539
+ * returned verbatim, since `{a,b}` repeated k times yields 2^k branches.
70540
+ */
70541
+ const MAX_BRACE_EXPANSIONS = 256;
70542
+ /** Matches an innermost brace group with at least one comma: `{a,b}`, not `{a}` or `{a,{b,c}}`. */
70543
+ const INNERMOST_BRACE_GROUP_REGEX = /\{([^{},]*(?:,[^{},]*)+)\}/;
70544
+ /**
70545
+ * Expands the comma alternations of a glob (`a.{ts,tsx}` → `a.ts`, `a.tsx`)
70546
+ * into one pattern per combination, innermost group first, so the result can
70547
+ * be handed to a consumer that splits its pattern list on bare commas. A glob
70548
+ * without a brace alternation is returned as is; a brace group without a
70549
+ * comma (e.g. `{a}`) is left untouched. A glob whose expansion would exceed
70550
+ * `MAX_BRACE_EXPANSIONS` patterns is returned as is as well.
70551
+ *
70552
+ * The cap is enforced on the work list itself rather than by counting the
70553
+ * groups up front: a nested group such as `{{a,b},c}` is rewritten into one
70554
+ * new group per inner alternative, so the number of patterns in flight is the
70555
+ * only bound that holds for arbitrary nesting.
70556
+ *
70557
+ * @example
70558
+ * expandBraceAlternations("src/**\/*.{ts,tsx}")
70559
+ * // => ["src/**\/*.ts", "src/**\/*.tsx"]
70560
+ */
70561
+ const expandBraceAlternations = (glob) => {
70562
+ let pending = [glob];
70563
+ for (;;) {
70564
+ const next = [];
70565
+ let expanded = false;
70566
+ for (const pattern of pending) {
70567
+ const match = INNERMOST_BRACE_GROUP_REGEX.exec(pattern);
70568
+ if (match === null) {
70569
+ next.push(pattern);
70570
+ continue;
70571
+ }
70572
+ expanded = true;
70573
+ const prefix = pattern.slice(0, match.index);
70574
+ const suffix = pattern.slice(match.index + match[0].length);
70575
+ for (const alternative of (match[1] ?? "").split(",")) next.push(`${prefix}${alternative}${suffix}`);
70576
+ if (next.length > MAX_BRACE_EXPANSIONS) return [glob];
70577
+ }
70578
+ if (!expanded) return [...new Set(next)];
70579
+ pending = next;
70580
+ }
70581
+ };
70582
+ //#endregion
70426
70583
  //#region src/features/rules/aiassistant-rule.ts
70427
70584
  /**
70585
+ * Rule types AI Assistant recognizes on the `apply:` metadata line. The values
70586
+ * are matched case-sensitively by the plugin, so they are emitted verbatim.
70587
+ * Any other value is carried through untouched (the plugin treats it as Off
70588
+ * today; a future build may recognize it).
70589
+ */
70590
+ const AIASSISTANT_APPLY_VALUES = [
70591
+ "always",
70592
+ "manually",
70593
+ "by model decision",
70594
+ "by file patterns",
70595
+ "off"
70596
+ ];
70597
+ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
70598
+ /**
70599
+ * Globs that match the whole project and so say nothing beyond "always".
70600
+ */
70601
+ const UNIVERSAL_GLOBS$2 = /* @__PURE__ */ new Set(["**/*", "*"]);
70602
+ const METADATA_DELIMITER = "---";
70603
+ /**
70604
+ * Splits text into lines that keep their own line terminator, so the body can
70605
+ * be sliced back out of the file byte-for-byte. Lone `\r` counts as a break,
70606
+ * matching Kotlin's `lines()`, which the plugin uses on the block.
70607
+ */
70608
+ const LINE_SPLIT_REGEX = /(?<=\r\n|\r(?!\n)|(?<!\r)\n)/;
70609
+ /**
70610
+ * The block is line-based, so a value must not carry a line break: one would
70611
+ * start a new `<field>: <value>` line and could override `apply:`.
70612
+ */
70613
+ const flattenValue = (value) => value.split(/\r\n|\r|\n/).map((part) => part.trim()).filter((part) => part.length > 0).join(" ");
70614
+ /**
70428
70615
  * Rule generator for JetBrains AI Assistant.
70429
70616
  *
70430
70617
  * AI Assistant reads project rules as flat Markdown files in
70431
- * `.aiassistant/rules/*.md` (one file per rule, identified by filename). The rule
70432
- * type (Always / Manually / By model decision / By file patterns) is configured
70433
- * in the IDE, not in the file, so the generated files are plain Markdown with no
70434
- * frontmatter. There is no special root file (unlike Junie's `guidelines.md`), so
70435
- * every rule — root or non-root — is written as its own file under `rules/`.
70618
+ * `.aiassistant/rules/*.md` (one file per rule, identified by filename). There
70619
+ * is no special root file (unlike Junie's `guidelines.md`), so every rule —
70620
+ * root or non-root — is written as its own file under `rules/`.
70621
+ *
70622
+ * The rule type (Always / Manually / By model decision / By file patterns /
70623
+ * Off) is stored in the file itself as a leading `---` metadata block that the
70624
+ * IDE's "Rule type" selector edits in place. The block is not YAML: each line
70625
+ * is `<field>: <value>` split on the first colon, and `patterns` is a
70626
+ * comma-separated glob list, so it is written and parsed by hand here. A file
70627
+ * without an `apply:` line is treated as Off at retrieval time (it is never
70628
+ * attached automatically), which is why every generated file carries one.
70629
+ *
70630
+ * Field names and values come from the plugin (`AiRulesService`,
70631
+ * `AiRulesLLMBundle.properties`; verified on build 262.10968.75) — the docs
70632
+ * page describes the rule type only as an IDE setting.
70436
70633
  *
70437
70634
  * AI Assistant and Junie are different JetBrains products (real-time assistance
70438
70635
  * vs. autonomous agent) with different layouts, so this is a separate target.
@@ -70440,18 +70637,154 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
70440
70637
  * @see https://www.jetbrains.com/help/ai-assistant/configure-project-rules.html
70441
70638
  */
70442
70639
  var AiassistantRule = class AiassistantRule extends ToolRule {
70640
+ body;
70641
+ metadata;
70642
+ constructor({ body, metadata, ...rest }) {
70643
+ super({
70644
+ ...rest,
70645
+ fileContent: AiassistantRule.buildFileContent({
70646
+ body,
70647
+ metadata
70648
+ })
70649
+ });
70650
+ this.body = body;
70651
+ this.metadata = metadata;
70652
+ }
70443
70653
  static getSettablePaths(_options = {}) {
70444
70654
  return { nonRoot: { relativeDirPath: AIASSISTANT_RULES_DIR_PATH } };
70445
70655
  }
70656
+ getBody() {
70657
+ return this.body;
70658
+ }
70659
+ getMetadata() {
70660
+ return this.metadata;
70661
+ }
70662
+ /**
70663
+ * Serialize the metadata block the way the IDE writes it
70664
+ * (`---\n<field>: <value>\n---`), followed by the body.
70665
+ */
70666
+ static buildFileContent({ body, metadata }) {
70667
+ if (metadata === void 0) return body;
70668
+ const lines = [`apply: ${metadata.apply}`];
70669
+ if (metadata.apply === "by model decision" && metadata.instructions) lines.push(`instructions: ${flattenValue(metadata.instructions)}`);
70670
+ if (metadata.apply === "by file patterns" && metadata.patterns && metadata.patterns.length > 0) lines.push(`patterns: ${metadata.patterns.map(flattenValue).join(", ")}`);
70671
+ return `${METADATA_DELIMITER}\n${lines.join("\n")}\n${METADATA_DELIMITER}\n\n${body}`;
70672
+ }
70673
+ /**
70674
+ * Split a rule file into its metadata block and body. The plugin locates the
70675
+ * block with `^---\s*([\s\S]*?)---\s*` (anchored at the very start of the
70676
+ * file); this reads it line by line instead — a delimiter is a line that is
70677
+ * exactly `---` — which is linear on any input and tolerates a BOM or blank
70678
+ * lines before the block. Each line is split on its first colon and trimmed;
70679
+ * unknown fields are ignored; a missing block or `apply:` line yields no
70680
+ * metadata, while an unrecognized `apply:` value is kept verbatim.
70681
+ */
70682
+ static parseFileContent(fileContent) {
70683
+ const content = fileContent.replace(/^\uFEFF/, "").trimStart();
70684
+ const lines = content.split(LINE_SPLIT_REGEX);
70685
+ const closingIndex = lines[0]?.trim() === METADATA_DELIMITER ? lines.findIndex((line, index) => index > 0 && line.trim() === METADATA_DELIMITER) : -1;
70686
+ if (closingIndex === -1) return {
70687
+ metadata: void 0,
70688
+ body: content.trim()
70689
+ };
70690
+ const body = lines.slice(closingIndex + 1).join("").trim();
70691
+ const fields = /* @__PURE__ */ new Map();
70692
+ for (const line of lines.slice(1, closingIndex)) {
70693
+ const separatorIndex = line.indexOf(":");
70694
+ if (separatorIndex === -1) continue;
70695
+ fields.set(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
70696
+ }
70697
+ const apply = fields.get("apply");
70698
+ if (apply === void 0 || apply.length === 0) return {
70699
+ metadata: void 0,
70700
+ body
70701
+ };
70702
+ const instructions = fields.get("instructions");
70703
+ const patterns = fields.get("patterns");
70704
+ return {
70705
+ metadata: {
70706
+ apply,
70707
+ ...apply === "by model decision" && instructions && { instructions },
70708
+ ...apply === "by file patterns" && patterns && { patterns: splitBraceAwareList(patterns) }
70709
+ },
70710
+ body
70711
+ };
70712
+ }
70713
+ /**
70714
+ * Derive the metadata block from the canonical frontmatter, mirroring the
70715
+ * Cursor mapping: an explicit `aiassistant.apply` wins; otherwise specific
70716
+ * `globs` become `by file patterns`, a `description` on a rule with no
70717
+ * `globs` at all becomes the `by model decision` condition, and everything
70718
+ * else — the root rule, universal globs (which already say "every file",
70719
+ * whatever the description), bare rules — is `always`.
70720
+ *
70721
+ * The plugin splits `patterns` on every comma, so a brace alternation such
70722
+ * as `*.{ts,tsx}` is expanded into one glob per branch before it is written.
70723
+ */
70724
+ static buildMetadata(frontmatter) {
70725
+ const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
70726
+ const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$2.has(glob));
70727
+ const instructions = frontmatter.description && flattenValue(frontmatter.description);
70728
+ const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
70729
+ const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
70730
+ const patterns = globs;
70731
+ return {
70732
+ apply,
70733
+ ...apply === "by model decision" && instructions && { instructions },
70734
+ ...apply === "by file patterns" && patterns.length > 0 && { patterns }
70735
+ };
70736
+ }
70737
+ /**
70738
+ * Warn, at generate time, about a block the plugin cannot act on as the
70739
+ * author presumably intended.
70740
+ */
70741
+ static warnAboutMetadata({ metadata, relativeFilePath }) {
70742
+ const { apply, instructions, patterns = [] } = metadata;
70743
+ if (!isAiassistantApply(apply)) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply "${apply}" is not one of ${AIASSISTANT_APPLY_VALUES.join(", ")}; it is written as is, and the current AI Assistant treats such a rule as Off.`);
70744
+ if (apply === "by file patterns" && patterns.length === 0) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by file patterns" but the rule has no globs, so AI Assistant will never attach it.`);
70745
+ if (apply === "by model decision" && !instructions) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by model decision" but the rule has no description, so AI Assistant has no condition to decide on.`);
70746
+ const commaGlob = patterns.find((glob) => glob.includes(","));
70747
+ if (commaGlob !== void 0) warnWithFallback(void 0, `${relativeFilePath}: the glob "${commaGlob}" contains a comma, which AI Assistant reads as a pattern separator.`);
70748
+ if ([
70749
+ apply,
70750
+ instructions ?? "",
70751
+ ...patterns
70752
+ ].some((value) => value.includes("---"))) warnWithFallback(void 0, `${relativeFilePath}: a metadata value contains "---", which AI Assistant reads as the end of the metadata block.`);
70753
+ }
70446
70754
  toRulesyncRule() {
70447
- return this.toRulesyncRuleDefault();
70755
+ const metadata = this.metadata;
70756
+ const globs = metadata?.apply === "by file patterns" ? metadata.patterns ?? [] : metadata?.apply === "always" ? ["**/*"] : [];
70757
+ const frontmatter = {
70758
+ root: false,
70759
+ targets: ["*"],
70760
+ description: metadata?.apply === "by model decision" ? metadata.instructions : void 0,
70761
+ globs
70762
+ };
70763
+ const derivedApply = metadata === void 0 ? void 0 : AiassistantRule.buildMetadata(frontmatter).apply;
70764
+ const apply = metadata !== void 0 && derivedApply !== metadata.apply ? metadata.apply : void 0;
70765
+ return new RulesyncRule({
70766
+ outputRoot: process.cwd(),
70767
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
70768
+ relativeFilePath: this.getRelativeFilePath(),
70769
+ frontmatter: {
70770
+ ...frontmatter,
70771
+ ...apply !== void 0 && { aiassistant: { apply } }
70772
+ },
70773
+ body: this.body
70774
+ });
70448
70775
  }
70449
70776
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
70777
+ const metadata = this.buildMetadata(rulesyncRule.getFrontmatter());
70778
+ this.warnAboutMetadata({
70779
+ metadata,
70780
+ relativeFilePath: rulesyncRule.getRelativeFilePath()
70781
+ });
70450
70782
  return new AiassistantRule({
70451
70783
  outputRoot,
70452
70784
  relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
70453
70785
  relativeFilePath: rulesyncRule.getRelativeFilePath(),
70454
- fileContent: rulesyncRule.getBody(),
70786
+ body: rulesyncRule.getBody(),
70787
+ metadata,
70455
70788
  validate,
70456
70789
  root: false
70457
70790
  });
@@ -70459,11 +70792,13 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
70459
70792
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
70460
70793
  const relativeDirPath = this.getSettablePaths().nonRoot.relativeDirPath;
70461
70794
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
70795
+ const { metadata, body } = this.parseFileContent(fileContent);
70462
70796
  return new AiassistantRule({
70463
70797
  outputRoot,
70464
70798
  relativeDirPath,
70465
70799
  relativeFilePath,
70466
- fileContent: fileContent.trim(),
70800
+ body,
70801
+ metadata,
70467
70802
  validate,
70468
70803
  root: false
70469
70804
  });
@@ -70473,7 +70808,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
70473
70808
  outputRoot,
70474
70809
  relativeDirPath,
70475
70810
  relativeFilePath,
70476
- fileContent: "",
70811
+ body: "",
70477
70812
  validate: false,
70478
70813
  root: false
70479
70814
  });
@@ -72030,40 +72365,14 @@ const CodebuddyRuleFrontmatterSchema = z.object({
72030
72365
  enabled: z.optional(z.boolean())
72031
72366
  });
72032
72367
  /**
72033
- * Splits a scalar `paths` value on the commas that separate patterns, leaving
72034
- * the commas inside a brace group alone: CodeBuddy's memory docs say "You can
72035
- * also combine multiple patterns with commas" and give
72036
- * `paths: {src,lib}/**\/*.ts, tests/**\/*.test.ts` as the example, where the
72037
- * first comma belongs to the brace expansion and the second separates the two
72038
- * globs. Only a scalar is split — the docs describe the comma form for the
72039
- * string shape, and a list already separates its patterns.
72040
- */
72041
- function splitCodebuddyPathsScalar(paths) {
72042
- const patterns = [];
72043
- let current = "";
72044
- let braceDepth = 0;
72045
- for (const char of paths) {
72046
- if (char === "{") braceDepth += 1;
72047
- else if (char === "}" && braceDepth > 0) braceDepth -= 1;
72048
- else if (char === "," && braceDepth === 0) {
72049
- patterns.push(current);
72050
- current = "";
72051
- continue;
72052
- }
72053
- current += char;
72054
- }
72055
- patterns.push(current);
72056
- return patterns.map((pattern) => pattern.trim());
72057
- }
72058
- /**
72059
72368
  * Normalizes the documented `string` / `string[]` shapes of `paths` to the
72060
72369
  * list form the rest of the adapter works with, splitting a comma-separated
72061
- * scalar into its patterns (see `splitCodebuddyPathsScalar`). An empty list
72370
+ * scalar into its patterns (see `splitBraceAwareList`). An empty list
72062
72371
  * and an empty string both mean "no paths".
72063
72372
  */
72064
72373
  function normalizeCodebuddyPaths(paths) {
72065
72374
  if (paths === void 0) return;
72066
- const list = (typeof paths === "string" ? splitCodebuddyPathsScalar(paths) : paths).filter((path) => path.trim() !== "");
72375
+ const list = (typeof paths === "string" ? splitBraceAwareList(paths) : paths).filter((path) => path.trim() !== "");
72067
72376
  return list.length > 0 ? list : void 0;
72068
72377
  }
72069
72378
  /**
@@ -80280,4 +80589,4 @@ async function importChecksCore(params) {
80280
80589
  //#endregion
80281
80590
  export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
80282
80591
 
80283
- //# sourceMappingURL=import-D_iukHSy.js.map
80592
+ //# sourceMappingURL=import-BnF2J75x.js.map