rulesync 9.7.0 → 10.0.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.
@@ -99,6 +99,9 @@ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH,
99
99
  const RULESYNC_MCP_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
100
100
  const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
101
101
  const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
102
+ const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
103
+ const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
104
+ const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
102
105
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
103
106
  const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
104
107
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
@@ -109,8 +112,12 @@ const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
109
112
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
110
113
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
111
114
  const RULESYNC_PERMISSIONS_FILE_NAME = "permissions.json";
115
+ const RULESYNC_MCP_JSONC_FILE_NAME = "mcp.jsonc";
116
+ const RULESYNC_HOOKS_JSONC_FILE_NAME = "hooks.jsonc";
117
+ const RULESYNC_PERMISSIONS_JSONC_FILE_NAME = "permissions.jsonc";
112
118
  const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
113
119
  const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
120
+ const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
114
121
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
115
122
  //#endregion
116
123
  //#region src/types/tool-target-tuples.ts
@@ -322,7 +329,8 @@ const hooksProcessorToolTargetTuple = [
322
329
  "junie",
323
330
  "vibe",
324
331
  "qwencode",
325
- "reasonix"
332
+ "reasonix",
333
+ "grokcli"
326
334
  ];
327
335
  const permissionsProcessorToolTargetTuple = [
328
336
  "amp",
@@ -6269,10 +6277,13 @@ const CLAUDE_HOOK_EVENTS = [
6269
6277
  * Hook events supported by Devin Local (native `.devin/` hooks).
6270
6278
  *
6271
6279
  * Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
6272
- * seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
6273
- * `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
6280
+ * eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
6281
+ * `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
6282
+ * `PostCompaction` (fires after context compaction; added in the Devin CLI
6283
+ * stable changelog — https://docs.devin.ai/cli/changelog/stable). The
6274
6284
  * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
6275
- * a `matcher` (regex against `tool_name`); the session/turn events do not.
6285
+ * a `matcher` (regex against `tool_name`); the session/turn/compaction events
6286
+ * do not.
6276
6287
  *
6277
6288
  * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
6278
6289
  * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
@@ -6287,7 +6298,8 @@ const DEVIN_HOOK_EVENTS = [
6287
6298
  "postToolUse",
6288
6299
  "beforeSubmitPrompt",
6289
6300
  "stop",
6290
- "permissionRequest"
6301
+ "permissionRequest",
6302
+ "postCompact"
6291
6303
  ];
6292
6304
  /** Hook events supported by OpenCode. */
6293
6305
  const OPENCODE_HOOK_EVENTS = [
@@ -6481,7 +6493,7 @@ const ANTIGRAVITY_HOOK_EVENTS = [
6481
6493
  * Hook events supported by AugmentCode (Auggie CLI).
6482
6494
  * Auggie mirrors Claude Code's lifecycle hooks but exposes a smaller set:
6483
6495
  * PreToolUse / PostToolUse (tool events, matcher-aware) plus the
6484
- * SessionStart / SessionEnd / Stop session events (no matcher).
6496
+ * SessionStart / SessionEnd / Stop / Notification session events (no matcher).
6485
6497
  * @see https://docs.augmentcode.com/cli/hooks
6486
6498
  */
6487
6499
  const AUGMENTCODE_HOOK_EVENTS = [
@@ -6489,7 +6501,8 @@ const AUGMENTCODE_HOOK_EVENTS = [
6489
6501
  "postToolUse",
6490
6502
  "sessionStart",
6491
6503
  "sessionEnd",
6492
- "stop"
6504
+ "stop",
6505
+ "notification"
6493
6506
  ];
6494
6507
  /**
6495
6508
  * Hook events supported by Mistral Vibe (mistral-vibe).
@@ -6541,6 +6554,10 @@ const JUNIE_HOOK_EVENTS = [
6541
6554
  * The Qwen-specific events
6542
6555
  * `TodoCreated`, `TodoCompleted`, and `StopFailure` map to the canonical
6543
6556
  * `todoCreated`, `todoCompleted`, and `stopFailure` events respectively.
6557
+ * Qwen's `HookEventName` enum (`packages/core/src/hooks/types.ts`) also documents
6558
+ * `PostToolBatch`, `UserPromptExpansion`, `PermissionDenied`, and
6559
+ * `InstructionsLoaded`, which map to the canonical `postToolBatch`,
6560
+ * `userPromptExpansion`, `permissionDenied`, and `instructionsLoaded` events.
6544
6561
  * @see https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
6545
6562
  */
6546
6563
  const QWENCODE_HOOK_EVENTS = [
@@ -6549,7 +6566,9 @@ const QWENCODE_HOOK_EVENTS = [
6549
6566
  "preToolUse",
6550
6567
  "postToolUse",
6551
6568
  "postToolUseFailure",
6569
+ "postToolBatch",
6552
6570
  "beforeSubmitPrompt",
6571
+ "userPromptExpansion",
6553
6572
  "stop",
6554
6573
  "stopFailure",
6555
6574
  "subagentStart",
@@ -6557,7 +6576,9 @@ const QWENCODE_HOOK_EVENTS = [
6557
6576
  "preCompact",
6558
6577
  "postCompact",
6559
6578
  "permissionRequest",
6579
+ "permissionDenied",
6560
6580
  "notification",
6581
+ "instructionsLoaded",
6561
6582
  "todoCreated",
6562
6583
  "todoCompleted"
6563
6584
  ];
@@ -6567,11 +6588,11 @@ const QWENCODE_HOOK_EVENTS = [
6567
6588
  * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
6568
6589
  * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
6569
6590
  * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
6570
- * `SubagentStop`, `Notification`, `PreCompact`). The eight events with a clean
6571
- * canonical equivalent are mapped: `PreToolUse`, `PostToolUse`,
6591
+ * `SubagentStop`, `Notification`, `PreCompact`). All ten have a clean canonical
6592
+ * equivalent and are mapped: `PreToolUse`, `PostToolUse`,
6572
6593
  * `UserPromptSubmit` ← `beforeSubmitPrompt`, `Stop`, `SessionStart`,
6573
- * `SessionEnd`, `SubagentStop`, and `PostLLMCall` ← `postModelInvocation`.
6574
- * (`Notification` and `PreCompact` have no canonical event and are left out.)
6594
+ * `SessionEnd`, `SubagentStop`, `PostLLMCall` ← `postModelInvocation`,
6595
+ * `Notification` ← `notification`, and `PreCompact` `preCompact`.
6575
6596
  * `match` (Reasonix's matcher field name) is honored only on
6576
6597
  * `PreToolUse`/`PostToolUse`, matching the canonical `matcher` field's
6577
6598
  * tool-event scoping used by other adapters.
@@ -6585,7 +6606,39 @@ const REASONIX_HOOK_EVENTS = [
6585
6606
  "sessionStart",
6586
6607
  "sessionEnd",
6587
6608
  "subagentStop",
6588
- "postModelInvocation"
6609
+ "postModelInvocation",
6610
+ "notification",
6611
+ "preCompact"
6612
+ ];
6613
+ /**
6614
+ * Hook events supported by Grok CLI (xAI Grok Build).
6615
+ *
6616
+ * Grok Build documents a Claude-Code-compatible hooks surface with fourteen
6617
+ * PascalCase events, all of which map 1:1 onto an existing canonical arm:
6618
+ * `SessionStart`, `SessionEnd`, `UserPromptSubmit` ← `beforeSubmitPrompt`,
6619
+ * `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionDenied`,
6620
+ * `Stop`, `StopFailure`, `Notification`, `SubagentStart`, `SubagentStop`,
6621
+ * `PreCompact`, `PostCompact`. A `matcher` (a regex tested against the tool
6622
+ * name) is meaningful only on the tool-name events (`PreToolUse`, `PostToolUse`,
6623
+ * `PostToolUseFailure`, `PermissionDenied`), matching Claude Code's semantics;
6624
+ * the remaining lifecycle events are matcher-less.
6625
+ * @see https://docs.x.ai/build/features/hooks
6626
+ */
6627
+ const GROKCLI_HOOK_EVENTS = [
6628
+ "sessionStart",
6629
+ "sessionEnd",
6630
+ "beforeSubmitPrompt",
6631
+ "preToolUse",
6632
+ "postToolUse",
6633
+ "postToolUseFailure",
6634
+ "permissionDenied",
6635
+ "stop",
6636
+ "stopFailure",
6637
+ "notification",
6638
+ "subagentStart",
6639
+ "subagentStop",
6640
+ "preCompact",
6641
+ "postCompact"
6589
6642
  ];
6590
6643
  /**
6591
6644
  * Hook events supported by Hermes Agent's native Shell Hooks system.
@@ -6659,6 +6712,7 @@ const HooksConfigSchema = z.looseObject({
6659
6712
  junie: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6660
6713
  vibe: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6661
6714
  reasonix: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6715
+ grokcli: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6662
6716
  qwencode: z.optional(z.looseObject({
6663
6717
  hooks: z.optional(hooksRecordSchema),
6664
6718
  disableAllHooks: z.optional(z.boolean())
@@ -6717,7 +6771,8 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6717
6771
  postToolUse: "PostToolUse",
6718
6772
  beforeSubmitPrompt: "UserPromptSubmit",
6719
6773
  stop: "Stop",
6720
- permissionRequest: "PermissionRequest"
6774
+ permissionRequest: "PermissionRequest",
6775
+ postCompact: "PostCompaction"
6721
6776
  };
6722
6777
  /**
6723
6778
  * Map Devin Local PascalCase event names to canonical camelCase.
@@ -6732,7 +6787,8 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6732
6787
  postToolUse: "PostToolUse",
6733
6788
  sessionStart: "SessionStart",
6734
6789
  sessionEnd: "SessionEnd",
6735
- stop: "Stop"
6790
+ stop: "Stop",
6791
+ notification: "Notification"
6736
6792
  };
6737
6793
  /**
6738
6794
  * Map AugmentCode PascalCase event names to canonical camelCase.
@@ -7002,7 +7058,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
7002
7058
  preToolUse: "PreToolUse",
7003
7059
  postToolUse: "PostToolUse",
7004
7060
  postToolUseFailure: "PostToolUseFailure",
7061
+ postToolBatch: "PostToolBatch",
7005
7062
  beforeSubmitPrompt: "UserPromptSubmit",
7063
+ userPromptExpansion: "UserPromptExpansion",
7006
7064
  stop: "Stop",
7007
7065
  subagentStart: "SubagentStart",
7008
7066
  subagentStop: "SubagentStop",
@@ -7010,7 +7068,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
7010
7068
  preCompact: "PreCompact",
7011
7069
  postCompact: "PostCompact",
7012
7070
  permissionRequest: "PermissionRequest",
7071
+ permissionDenied: "PermissionDenied",
7013
7072
  notification: "Notification",
7073
+ instructionsLoaded: "InstructionsLoaded",
7014
7074
  todoCreated: "TodoCreated",
7015
7075
  todoCompleted: "TodoCompleted"
7016
7076
  };
@@ -7032,12 +7092,40 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
7032
7092
  sessionStart: "SessionStart",
7033
7093
  sessionEnd: "SessionEnd",
7034
7094
  subagentStop: "SubagentStop",
7035
- postModelInvocation: "PostLLMCall"
7095
+ postModelInvocation: "PostLLMCall",
7096
+ notification: "Notification",
7097
+ preCompact: "PreCompact"
7036
7098
  };
7037
7099
  /**
7038
7100
  * Map Reasonix PascalCase event names to canonical camelCase.
7039
7101
  */
7040
7102
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
7103
+ /**
7104
+ * Map canonical camelCase event names to Grok CLI PascalCase.
7105
+ * Grok Build reuses the same Claude-style PascalCase event names, so each
7106
+ * canonical arm maps to its PascalCase equivalent.
7107
+ * @see https://docs.x.ai/build/features/hooks
7108
+ */
7109
+ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
7110
+ sessionStart: "SessionStart",
7111
+ sessionEnd: "SessionEnd",
7112
+ beforeSubmitPrompt: "UserPromptSubmit",
7113
+ preToolUse: "PreToolUse",
7114
+ postToolUse: "PostToolUse",
7115
+ postToolUseFailure: "PostToolUseFailure",
7116
+ permissionDenied: "PermissionDenied",
7117
+ stop: "Stop",
7118
+ stopFailure: "StopFailure",
7119
+ notification: "Notification",
7120
+ subagentStart: "SubagentStart",
7121
+ subagentStop: "SubagentStop",
7122
+ preCompact: "PreCompact",
7123
+ postCompact: "PostCompact"
7124
+ };
7125
+ /**
7126
+ * Map Grok CLI PascalCase event names to canonical camelCase.
7127
+ */
7128
+ const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
7041
7129
  //#endregion
7042
7130
  //#region src/features/hooks/tool-hooks-converter.ts
7043
7131
  function isToolMatcherEntry(x) {
@@ -7236,12 +7324,55 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
7236
7324
  return canonical;
7237
7325
  }
7238
7326
  //#endregion
7327
+ //#region src/utils/jsonc.ts
7328
+ /**
7329
+ * Rebuild the parsed value from its own enumerable entries, dropping
7330
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
7331
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
7332
+ * literal `__proto__` key would replace the containing object's prototype
7333
+ * instead of becoming an own property; rebuilding gives every object a clean
7334
+ * `Object.prototype` again and severs that path.
7335
+ */
7336
+ function deepSanitize(value) {
7337
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
7338
+ if (value !== null && typeof value === "object") {
7339
+ const sanitized = {};
7340
+ for (const [key, entry] of Object.entries(value)) {
7341
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
7342
+ sanitized[key] = deepSanitize(entry);
7343
+ }
7344
+ return sanitized;
7345
+ }
7346
+ return value;
7347
+ }
7348
+ /**
7349
+ * Parse a JSONC (JSON with Comments) document strictly.
7350
+ *
7351
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
7352
+ * errors and returns a best-effort value), this throws on any parse error so
7353
+ * a malformed source file fails loudly instead of generating half-empty tool
7354
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
7355
+ * `JSON.parse` on files that may contain comments or trailing commas.
7356
+ */
7357
+ function parseJsonc(content) {
7358
+ const errors = [];
7359
+ const result = parse(content, errors, {
7360
+ allowTrailingComma: true,
7361
+ disallowComments: false
7362
+ });
7363
+ if (errors.length > 0) {
7364
+ const details = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
7365
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
7366
+ }
7367
+ return deepSanitize(result);
7368
+ }
7369
+ //#endregion
7239
7370
  //#region src/features/hooks/rulesync-hooks.ts
7240
7371
  var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7241
7372
  json;
7242
7373
  constructor(params) {
7243
7374
  super({ ...params });
7244
- this.json = JSON.parse(this.fileContent);
7375
+ this.json = parseJsonc(this.fileContent);
7245
7376
  if (params.validate) {
7246
7377
  const result = this.validate();
7247
7378
  if (!result.success) throw result.error;
@@ -7250,7 +7381,11 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7250
7381
  static getSettablePaths() {
7251
7382
  return {
7252
7383
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7253
- relativeFilePath: "hooks.json"
7384
+ relativeFilePath: RULESYNC_HOOKS_FILE_NAME,
7385
+ jsonc: {
7386
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7387
+ relativeFilePath: RULESYNC_HOOKS_JSONC_FILE_NAME
7388
+ }
7254
7389
  };
7255
7390
  }
7256
7391
  validate() {
@@ -7266,16 +7401,23 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7266
7401
  }
7267
7402
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
7268
7403
  const paths = RulesyncHooks.getSettablePaths();
7269
- const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7270
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} found.`);
7271
- const fileContent = await readFileContent(filePath);
7272
- return new RulesyncHooks({
7273
- outputRoot,
7404
+ const candidates = [paths.jsonc, {
7274
7405
  relativeDirPath: paths.relativeDirPath,
7275
- relativeFilePath: paths.relativeFilePath,
7276
- fileContent,
7277
- validate
7278
- });
7406
+ relativeFilePath: paths.relativeFilePath
7407
+ }];
7408
+ for (const candidate of candidates) {
7409
+ const filePath = join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
7410
+ if (!await fileExists(filePath)) continue;
7411
+ const fileContent = await readFileContent(filePath);
7412
+ return new RulesyncHooks({
7413
+ outputRoot,
7414
+ relativeDirPath: candidate.relativeDirPath,
7415
+ relativeFilePath: candidate.relativeFilePath,
7416
+ fileContent,
7417
+ validate
7418
+ });
7419
+ }
7420
+ throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} or ${RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH} found.`);
7279
7421
  }
7280
7422
  getJson() {
7281
7423
  return this.json;
@@ -7562,7 +7704,8 @@ const AUGMENTCODE_CONVERTER_CONFIG = {
7562
7704
  noMatcherEvents: /* @__PURE__ */ new Set([
7563
7705
  "sessionStart",
7564
7706
  "sessionEnd",
7565
- "stop"
7707
+ "stop",
7708
+ "notification"
7566
7709
  ])
7567
7710
  };
7568
7711
  /**
@@ -8661,7 +8804,8 @@ const DEVIN_CONVERTER_CONFIG = {
8661
8804
  "sessionStart",
8662
8805
  "sessionEnd",
8663
8806
  "stop",
8664
- "beforeSubmitPrompt"
8807
+ "beforeSubmitPrompt",
8808
+ "postCompact"
8665
8809
  ])
8666
8810
  };
8667
8811
  /**
@@ -8970,6 +9114,167 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8970
9114
  }
8971
9115
  };
8972
9116
  //#endregion
9117
+ //#region src/constants/grokcli-paths.ts
9118
+ /**
9119
+ * Grok Build CLI (xAI) configuration-layout conventions.
9120
+ *
9121
+ * Single source of truth for where Grok Build expects its files. Grok Build
9122
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9123
+ * with project/global scopes resolved by the directory the CLI runs in
9124
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9125
+ *
9126
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9127
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9128
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9129
+ * @see https://docs.x.ai/build/overview
9130
+ */
9131
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9132
+ const GROKCLI_DIR = ".grok";
9133
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9134
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9135
+ /**
9136
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9137
+ * permission mode, and other settings all live here; permissions reuse the same
9138
+ * file name as MCP since Grok consolidates everything into one config.
9139
+ */
9140
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9141
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9142
+ const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
9143
+ /**
9144
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9145
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9146
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9147
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9148
+ * @see https://docs.x.ai/build/features/hooks
9149
+ */
9150
+ const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
9151
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9152
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9153
+ /**
9154
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9155
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9156
+ * (global), each a Markdown file with YAML frontmatter (verified via
9157
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9158
+ */
9159
+ const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
9160
+ /**
9161
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9162
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9163
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9164
+ * MCP/skills/subagents adapters).
9165
+ */
9166
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9167
+ //#endregion
9168
+ //#region src/features/hooks/grokcli-hooks.ts
9169
+ const GROKCLI_CONVERTER_CONFIG = {
9170
+ supportedEvents: GROKCLI_HOOK_EVENTS,
9171
+ canonicalToToolEventNames: CANONICAL_TO_GROKCLI_EVENT_NAMES,
9172
+ toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
9173
+ projectDirVar: "",
9174
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
9175
+ noMatcherEvents: /* @__PURE__ */ new Set([
9176
+ "sessionStart",
9177
+ "sessionEnd",
9178
+ "beforeSubmitPrompt",
9179
+ "stop",
9180
+ "stopFailure",
9181
+ "notification",
9182
+ "subagentStart",
9183
+ "subagentStop",
9184
+ "preCompact",
9185
+ "postCompact"
9186
+ ])
9187
+ };
9188
+ /**
9189
+ * Hooks generator for Grok CLI (xAI Grok Build).
9190
+ *
9191
+ * Grok Build adopts a Claude-Code-compatible lifecycle hooks model: each event
9192
+ * maps to an array of `{ matcher?, hooks: [{ type, command, timeout? }] }`
9193
+ * matcher groups under a top-level `hooks` key. rulesync writes all its hooks
9194
+ * into a single standalone `rulesync.json` file discovered from
9195
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global). The file
9196
+ * is dedicated to hooks and owned wholesale by rulesync, so it may be deleted
9197
+ * as an orphan.
9198
+ *
9199
+ * @see https://docs.x.ai/build/features/hooks
9200
+ */
9201
+ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
9202
+ constructor(params) {
9203
+ super({
9204
+ ...params,
9205
+ fileContent: params.fileContent ?? "{}"
9206
+ });
9207
+ }
9208
+ static getSettablePaths(_options = {}) {
9209
+ return {
9210
+ relativeDirPath: GROKCLI_HOOKS_DIR_PATH,
9211
+ relativeFilePath: GROKCLI_HOOKS_FILE_NAME
9212
+ };
9213
+ }
9214
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9215
+ const paths = GrokcliHooks.getSettablePaths({ global });
9216
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9217
+ return new GrokcliHooks({
9218
+ outputRoot,
9219
+ relativeDirPath: paths.relativeDirPath,
9220
+ relativeFilePath: paths.relativeFilePath,
9221
+ fileContent,
9222
+ validate
9223
+ });
9224
+ }
9225
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9226
+ const paths = GrokcliHooks.getSettablePaths({ global });
9227
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9228
+ const config = rulesyncHooks.getJson();
9229
+ const grokHooks = canonicalToToolHooks({
9230
+ config,
9231
+ toolOverrideHooks: config.grokcli?.hooks,
9232
+ converterConfig: GROKCLI_CONVERTER_CONFIG,
9233
+ logger
9234
+ });
9235
+ await readOrInitializeFileContent(filePath, JSON.stringify({ hooks: {} }, null, 2));
9236
+ const fileContent = JSON.stringify({ hooks: grokHooks }, null, 2);
9237
+ return new GrokcliHooks({
9238
+ outputRoot,
9239
+ relativeDirPath: paths.relativeDirPath,
9240
+ relativeFilePath: paths.relativeFilePath,
9241
+ fileContent,
9242
+ validate
9243
+ });
9244
+ }
9245
+ toRulesyncHooks() {
9246
+ let parsed;
9247
+ try {
9248
+ parsed = JSON.parse(this.getFileContent());
9249
+ } catch (error) {
9250
+ throw new Error(`Failed to parse Grok hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9251
+ }
9252
+ const hooks = toolHooksToCanonical({
9253
+ hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
9254
+ converterConfig: GROKCLI_CONVERTER_CONFIG
9255
+ });
9256
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9257
+ version: 1,
9258
+ hooks
9259
+ }, null, 2) });
9260
+ }
9261
+ validate() {
9262
+ return {
9263
+ success: true,
9264
+ error: null
9265
+ };
9266
+ }
9267
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9268
+ return new GrokcliHooks({
9269
+ outputRoot,
9270
+ relativeDirPath,
9271
+ relativeFilePath,
9272
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9273
+ validate: false
9274
+ });
9275
+ }
9276
+ };
9277
+ //#endregion
8973
9278
  //#region src/features/hooks/hermesagent-hooks.ts
8974
9279
  /**
8975
9280
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -10176,9 +10481,9 @@ function reasonixHooksToCanonical(hooks) {
10176
10481
  * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
10177
10482
  * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
10178
10483
  * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
10179
- * The eight upstream events with a clean canonical equivalent are mapped:
10484
+ * All ten upstream events have a clean canonical equivalent and are mapped:
10180
10485
  * PreToolUse/PostToolUse/UserPromptSubmit/Stop plus SessionStart/SessionEnd/
10181
- * SubagentStop/PostLLMCall (see REASONIX_HOOK_EVENTS).
10486
+ * SubagentStop/PostLLMCall/Notification/PreCompact (see REASONIX_HOOK_EVENTS).
10182
10487
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
10183
10488
  */
10184
10489
  var ReasonixHooks = class ReasonixHooks extends ToolHooks {
@@ -10766,6 +11071,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
10766
11071
  supportedEvents: REASONIX_HOOK_EVENTS,
10767
11072
  supportedHookTypes: ["command"],
10768
11073
  supportsMatcher: true
11074
+ }],
11075
+ ["grokcli", {
11076
+ class: GrokcliHooks,
11077
+ meta: {
11078
+ supportsProject: true,
11079
+ supportsGlobal: true,
11080
+ supportsImport: true
11081
+ },
11082
+ supportedEvents: GROKCLI_HOOK_EVENTS,
11083
+ supportedHookTypes: ["command"],
11084
+ supportsMatcher: true
10769
11085
  }]
10770
11086
  ]);
10771
11087
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -12109,15 +12425,51 @@ const RulesyncMcpServerSchema = z.extend(McpServerSchema, {
12109
12425
  exposed: z.optional(z.boolean())
12110
12426
  });
12111
12427
  const RulesyncMcpConfigSchema = z.object({ mcpServers: z.record(z.string(), RulesyncMcpServerSchema) });
12428
+ /**
12429
+ * Tool-scoped MCP block: servers that apply only to one tool. A named entry
12430
+ * replaces/adds the same-named shared server wholesale for that tool; `null`
12431
+ * removes the shared server for that tool. Mirrors `{toolname}.hooks` in
12432
+ * `.rulesync/hooks.json` and `{toolname}.permission` in
12433
+ * `.rulesync/permissions.json`.
12434
+ */
12435
+ const toolScopedMcpSchema = z.looseObject({ mcpServers: z.optional(z.record(z.string(), z.nullable(RulesyncMcpServerSchema))) });
12112
12436
  const RulesyncMcpFileSchema = z.looseObject({
12113
12437
  $schema: z.optional(z.string()),
12114
- ...RulesyncMcpConfigSchema.shape
12438
+ ...RulesyncMcpConfigSchema.shape,
12439
+ amp: z.optional(toolScopedMcpSchema),
12440
+ "antigravity-cli": z.optional(toolScopedMcpSchema),
12441
+ "antigravity-ide": z.optional(toolScopedMcpSchema),
12442
+ augmentcode: z.optional(toolScopedMcpSchema),
12443
+ claudecode: z.optional(toolScopedMcpSchema),
12444
+ cline: z.optional(toolScopedMcpSchema),
12445
+ codexcli: z.optional(toolScopedMcpSchema),
12446
+ copilot: z.optional(toolScopedMcpSchema),
12447
+ copilotcli: z.optional(toolScopedMcpSchema),
12448
+ cursor: z.optional(toolScopedMcpSchema),
12449
+ deepagents: z.optional(toolScopedMcpSchema),
12450
+ devin: z.optional(toolScopedMcpSchema),
12451
+ factorydroid: z.optional(toolScopedMcpSchema),
12452
+ goose: z.optional(toolScopedMcpSchema),
12453
+ grokcli: z.optional(toolScopedMcpSchema),
12454
+ hermesagent: z.optional(toolScopedMcpSchema),
12455
+ junie: z.optional(toolScopedMcpSchema),
12456
+ kilo: z.optional(toolScopedMcpSchema),
12457
+ kiro: z.optional(toolScopedMcpSchema),
12458
+ opencode: z.optional(toolScopedMcpSchema),
12459
+ qwencode: z.optional(toolScopedMcpSchema),
12460
+ reasonix: z.optional(toolScopedMcpSchema),
12461
+ roo: z.optional(toolScopedMcpSchema),
12462
+ rovodev: z.optional(toolScopedMcpSchema),
12463
+ takt: z.optional(toolScopedMcpSchema),
12464
+ vibe: z.optional(toolScopedMcpSchema),
12465
+ warp: z.optional(toolScopedMcpSchema),
12466
+ zed: z.optional(toolScopedMcpSchema)
12115
12467
  });
12116
12468
  var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12117
12469
  json;
12118
12470
  constructor(params) {
12119
12471
  super(params);
12120
- this.json = JSON.parse(this.fileContent);
12472
+ this.json = parseJsonc(this.fileContent);
12121
12473
  if (params.validate) {
12122
12474
  const result = this.validate();
12123
12475
  if (!result.success) throw result.error;
@@ -12127,7 +12479,11 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12127
12479
  return {
12128
12480
  recommended: {
12129
12481
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12130
- relativeFilePath: "mcp.json"
12482
+ relativeFilePath: RULESYNC_MCP_FILE_NAME
12483
+ },
12484
+ jsonc: {
12485
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12486
+ relativeFilePath: RULESYNC_MCP_JSONC_FILE_NAME
12131
12487
  },
12132
12488
  legacy: {
12133
12489
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
@@ -12149,7 +12505,18 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12149
12505
  static async fromFile({ outputRoot = process.cwd(), validate = true, logger }) {
12150
12506
  const paths = this.getSettablePaths();
12151
12507
  const recommendedPath = join(outputRoot, paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
12508
+ const jsoncPath = join(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
12152
12509
  const legacyPath = join(outputRoot, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
12510
+ if (await fileExists(jsoncPath)) {
12511
+ const fileContent = await readFileContent(jsoncPath);
12512
+ return new RulesyncMcp({
12513
+ outputRoot,
12514
+ relativeDirPath: paths.jsonc.relativeDirPath,
12515
+ relativeFilePath: paths.jsonc.relativeFilePath,
12516
+ fileContent,
12517
+ validate
12518
+ });
12519
+ }
12153
12520
  if (await fileExists(recommendedPath)) {
12154
12521
  const fileContent = await readFileContent(recommendedPath);
12155
12522
  return new RulesyncMcp({
@@ -12209,7 +12576,141 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12209
12576
  getJson() {
12210
12577
  return this.json;
12211
12578
  }
12212
- };
12579
+ /**
12580
+ * Build the effective MCP config for one tool target:
12581
+ *
12582
+ * 1. Filter shared servers by the DEPRECATED per-server `targets` field
12583
+ * (missing/`["*"]` means every tool). A deprecation warning points at
12584
+ * the tool-scoped blocks that replace it.
12585
+ * 2. Overlay the tool-scoped `{toolname}.mcpServers` block(s): a named
12586
+ * entry replaces/adds the shared server wholesale for this tool; `null`
12587
+ * removes it.
12588
+ *
12589
+ * Targets that share one output file resolve identically so the shared
12590
+ * file's content never depends on which of them generates last — see
12591
+ * `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
12592
+ * and the Antigravity pair).
12593
+ *
12594
+ * Returns the same instance when neither mechanism is used.
12595
+ */
12596
+ forTarget({ toolTarget, logger }) {
12597
+ const { blockKeys, acceptedTargetNames } = resolveMcpTarget({ toolTarget });
12598
+ const json = this.json;
12599
+ const sharedServers = this.json.mcpServers ?? {};
12600
+ const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
12601
+ if (serverNamesWithTargets.length > 0) this.warnTargetsDeprecationOnce({
12602
+ serverNamesWithTargets,
12603
+ logger
12604
+ });
12605
+ for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
12606
+ if (!isRecord(json[ignoredKey])) continue;
12607
+ 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);
12608
+ }
12609
+ const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
12610
+ if (serverNamesWithTargets.length === 0 && toolBlockKeys.length === 0) return this;
12611
+ const effectiveServers = Object.fromEntries(Object.entries(sharedServers).filter(([, serverConfig]) => {
12612
+ const targets = serverConfig.targets;
12613
+ if (targets === void 0) return true;
12614
+ return targets.some((target) => target === "*" || acceptedTargetNames.has(target));
12615
+ }));
12616
+ for (const blockKey of blockKeys) {
12617
+ const toolBlock = json[blockKey];
12618
+ const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
12619
+ for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
12620
+ if (isPrototypePollutionKey(serverName)) continue;
12621
+ if (serverConfig === null) delete effectiveServers[serverName];
12622
+ else effectiveServers[serverName] = serverConfig;
12623
+ }
12624
+ }
12625
+ const rest = Object.fromEntries(Object.entries(json).filter(([key]) => !MCP_TOOL_BLOCK_KEYS.has(key)));
12626
+ return new RulesyncMcp({
12627
+ outputRoot: this.outputRoot,
12628
+ relativeDirPath: this.relativeDirPath,
12629
+ relativeFilePath: this.relativeFilePath,
12630
+ fileContent: JSON.stringify({
12631
+ ...rest,
12632
+ mcpServers: effectiveServers
12633
+ }, null, 2)
12634
+ });
12635
+ }
12636
+ /**
12637
+ * The deprecation warning would otherwise repeat once per generated tool
12638
+ * target (a full `--targets "*"` run creates one RulesyncMcp per target),
12639
+ * so it is deduplicated per source file path.
12640
+ */
12641
+ warnTargetsDeprecationOnce({ serverNamesWithTargets, logger }) {
12642
+ this.warnOncePerFile("targets-deprecation", `The per-server "targets" field in ${join(this.relativeDirPath, this.relativeFilePath)} is deprecated (servers: ${serverNamesWithTargets.join(", ")}). Author tool-scoped "{toolname}.mcpServers" blocks instead.`, logger);
12643
+ }
12644
+ warnOncePerFile(kind, message, logger) {
12645
+ const dedupeKey = `${this.getFilePath()}#${kind}`;
12646
+ if (warnedOnceKeys.has(dedupeKey)) return;
12647
+ warnedOnceKeys.add(dedupeKey);
12648
+ logger?.warn(message);
12649
+ }
12650
+ };
12651
+ /**
12652
+ * All keys that may hold a tool-scoped `{toolname}.mcpServers` block. Derived
12653
+ * from the MCP processor's target tuple so a newly added MCP-capable tool is
12654
+ * covered automatically.
12655
+ */
12656
+ const MCP_TOOL_BLOCK_KEYS = new Set(mcpProcessorToolTargetTuple);
12657
+ /**
12658
+ * Targets whose `{toolname}.mcpServers` block key is ALWAYS another target's
12659
+ * key (at every scope): `claudecode-legacy` is a deprecated alias of
12660
+ * `claudecode`, and the Kiro IDE/CLI targets share the `kiro` block because
12661
+ * all three write the same `.kiro/settings/mcp.json` at both scopes. Blocks
12662
+ * authored under these source names are never read (a warning is emitted).
12663
+ */
12664
+ const MCP_BLOCK_KEY_ALIASES = {
12665
+ "claudecode-legacy": "claudecode",
12666
+ "kiro-cli": "kiro",
12667
+ "kiro-ide": "kiro"
12668
+ };
12669
+ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
12670
+ /**
12671
+ * Resolve which tool-scoped blocks a target reads and which deprecated
12672
+ * `targets` names match it. Targets that share one output file must resolve
12673
+ * identically, otherwise the shared file's content would depend on which
12674
+ * target happened to generate last:
12675
+ *
12676
+ * - `claudecode-legacy` aliases `claudecode`; the Kiro trio shares `kiro`
12677
+ * (same output file at both scopes).
12678
+ * - `antigravity-ide` / `antigravity-cli` share their output file at BOTH
12679
+ * scopes — `.agents/mcp_config.json` (project) and
12680
+ * `~/.gemini/config/mcp_config.json` (global; both global subdirs are
12681
+ * `config`) — so both targets always apply both blocks in a fixed order
12682
+ * (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
12683
+ * per server on conflict).
12684
+ */
12685
+ function resolveMcpTarget({ toolTarget }) {
12686
+ if (toolTarget === "claudecode" || toolTarget === "claudecode-legacy") return {
12687
+ blockKeys: ["claudecode"],
12688
+ acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
12689
+ };
12690
+ if (toolTarget === "kiro" || toolTarget === "kiro-cli" || toolTarget === "kiro-ide") return {
12691
+ blockKeys: ["kiro"],
12692
+ acceptedTargetNames: /* @__PURE__ */ new Set([
12693
+ "kiro",
12694
+ "kiro-cli",
12695
+ "kiro-ide"
12696
+ ])
12697
+ };
12698
+ if (toolTarget === "antigravity-ide" || toolTarget === "antigravity-cli") return {
12699
+ blockKeys: ["antigravity-ide", "antigravity-cli"],
12700
+ acceptedTargetNames: /* @__PURE__ */ new Set(["antigravity-ide", "antigravity-cli"])
12701
+ };
12702
+ return {
12703
+ blockKeys: [toolTarget],
12704
+ acceptedTargetNames: /* @__PURE__ */ new Set([toolTarget])
12705
+ };
12706
+ }
12707
+ /**
12708
+ * Deduplication set for once-per-source-file warnings, keyed by
12709
+ * `<absolute file path>#<warning kind>`. Never cleared: rulesync CLI runs
12710
+ * are one-shot processes, and in a long-lived embedding (the rulesync MCP
12711
+ * server) repeating the same warning per generate would only add noise.
12712
+ */
12713
+ const warnedOnceKeys = /* @__PURE__ */ new Set();
12213
12714
  //#endregion
12214
12715
  //#region src/features/mcp/tool-mcp.ts
12215
12716
  var ToolMcp = class extends ToolFile {
@@ -13021,7 +13522,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
13021
13522
  ...rest,
13022
13523
  validate: false
13023
13524
  });
13024
- this.toml = smolToml.parse(this.fileContent);
13525
+ let toml;
13526
+ try {
13527
+ toml = smolToml.parse(this.fileContent);
13528
+ } catch (error) {
13529
+ throw new Error(`Failed to parse Codex CLI config at ${this.getFilePath()}: ${formatError(error)}`, { cause: error });
13530
+ }
13531
+ this.toml = toml;
13025
13532
  if (rest.validate) {
13026
13533
  const result = this.validate();
13027
13534
  if (!result.success) throw result.error;
@@ -13057,7 +13564,12 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
13057
13564
  const paths = this.getSettablePaths({ global });
13058
13565
  const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13059
13566
  const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13060
- const configToml = smolToml.parse(configTomlFileContent || smolToml.stringify({}));
13567
+ let configToml;
13568
+ try {
13569
+ configToml = smolToml.parse(configTomlFileContent || smolToml.stringify({}));
13570
+ } catch (error) {
13571
+ throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
13572
+ }
13061
13573
  const strippedMcpServers = rulesyncMcp.getMcpServers();
13062
13574
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
13063
13575
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
@@ -14031,47 +14543,6 @@ var GooseMcp = class GooseMcp extends ToolMcp {
14031
14543
  }
14032
14544
  };
14033
14545
  //#endregion
14034
- //#region src/constants/grokcli-paths.ts
14035
- /**
14036
- * Grok Build CLI (xAI) configuration-layout conventions.
14037
- *
14038
- * Single source of truth for where Grok Build expects its files. Grok Build
14039
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
14040
- * with project/global scopes resolved by the directory the CLI runs in
14041
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
14042
- *
14043
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
14044
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
14045
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
14046
- * @see https://docs.x.ai/build/overview
14047
- */
14048
- /** Root directory for Grok Build configuration, relative to the scope root. */
14049
- const GROKCLI_DIR = ".grok";
14050
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
14051
- const GROKCLI_MCP_FILE_NAME = "config.toml";
14052
- /**
14053
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
14054
- * permission mode, and other settings all live here; permissions reuse the same
14055
- * file name as MCP since Grok consolidates everything into one config.
14056
- */
14057
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
14058
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
14059
- const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
14060
- /**
14061
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
14062
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
14063
- * (global), each a Markdown file with YAML frontmatter (verified via
14064
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
14065
- */
14066
- const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
14067
- /**
14068
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
14069
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
14070
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
14071
- * MCP/skills/subagents adapters).
14072
- */
14073
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
14074
- //#endregion
14075
14546
  //#region src/features/mcp/grokcli-mcp.ts
14076
14547
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
14077
14548
  /**
@@ -14231,6 +14702,54 @@ function resolveHermesTimeout(config) {
14231
14702
  if (typeof config.networkTimeout === "number") return config.networkTimeout;
14232
14703
  }
14233
14704
  /**
14705
+ * Copies the advanced Hermes-recognized per-server fields that have no canonical
14706
+ * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
14707
+ * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
14708
+ * `connect_timeout` (seconds), and `supports_parallel_tool_calls` — verbatim
14709
+ * from `source` to `target`. Field names are identical on both sides (the
14710
+ * canonical `McpServerSchema` is a `looseObject`), so this serves export and
14711
+ * import alike. See the Hermes mcp-config-reference.
14712
+ */
14713
+ function copyHermesAdvancedFields(source, target) {
14714
+ if (typeof source.auth === "string") target.auth = source.auth;
14715
+ if (typeof source.client_cert === "string" || isStringArray(source.client_cert)) target.client_cert = source.client_cert;
14716
+ if (typeof source.client_key === "string") target.client_key = source.client_key;
14717
+ if (typeof source.connect_timeout === "number") target.connect_timeout = source.connect_timeout;
14718
+ if (typeof source.supports_parallel_tool_calls === "boolean") target.supports_parallel_tool_calls = source.supports_parallel_tool_calls;
14719
+ }
14720
+ /**
14721
+ * Builds Hermes's per-server `tools` block from a canonical server config. The
14722
+ * canonical `enabledTools`/`disabledTools` arrays become `include`/`exclude`,
14723
+ * and the boolean `promptsEnabled`/`resourcesEnabled` toggles become Hermes's
14724
+ * `prompts`/`resources` capability flags. Returns an empty object when the
14725
+ * server has no tool scoping (the caller omits the block in that case).
14726
+ *
14727
+ * Note: `promptsEnabled`/`resourcesEnabled` are canonical top-level keys rather
14728
+ * than a nested canonical `tools` object, because canonical `McpServerSchema.tools`
14729
+ * is reserved as a `string[]` (used by other tools) — reusing it for an object
14730
+ * would fail validation on the next `generate`.
14731
+ */
14732
+ function buildHermesToolsBlock(config) {
14733
+ const tools = {};
14734
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
14735
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14736
+ if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
14737
+ if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
14738
+ return tools;
14739
+ }
14740
+ /**
14741
+ * Applies a Hermes per-server `tools` block back onto a canonical server config
14742
+ * (inverse of {@link buildHermesToolsBlock}): `include`/`exclude` become
14743
+ * `enabledTools`/`disabledTools`, and `prompts`/`resources` become the boolean
14744
+ * `promptsEnabled`/`resourcesEnabled` top-level toggles.
14745
+ */
14746
+ function applyHermesToolsBlock(hermesTools, server) {
14747
+ if (isStringArray(hermesTools.include)) server.enabledTools = hermesTools.include;
14748
+ if (isStringArray(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
14749
+ if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
14750
+ if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
14751
+ }
14752
+ /**
14234
14753
  * Converts a single rulesync canonical MCP server into a Hermes `mcp_servers:` entry.
14235
14754
  *
14236
14755
  * Hermes is close to the MCP spec but not identical: `command` must be a single
@@ -14264,9 +14783,8 @@ function convertServerToHermes(config) {
14264
14783
  if (config.disabled === true) out.enabled = false;
14265
14784
  const timeout = resolveHermesTimeout(config);
14266
14785
  if (timeout !== void 0) out.timeout = timeout;
14267
- const tools = {};
14268
- if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
14269
- if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14786
+ copyHermesAdvancedFields(config, out);
14787
+ const tools = buildHermesToolsBlock(config);
14270
14788
  if (Object.keys(tools).length > 0) out.tools = tools;
14271
14789
  return out;
14272
14790
  }
@@ -14309,10 +14827,8 @@ function convertFromHermesFormat(mcpServers) {
14309
14827
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
14310
14828
  if (config.enabled === false) server.disabled = true;
14311
14829
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
14312
- if (isRecord(config.tools)) {
14313
- if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
14314
- if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
14315
- }
14830
+ copyHermesAdvancedFields(config, server);
14831
+ if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
14316
14832
  result[name] = server;
14317
14833
  }
14318
14834
  return result;
@@ -16444,10 +16960,14 @@ var McpProcessor = class extends FeatureProcessor {
16444
16960
  if (!rulesyncMcp) throw new Error(`No ${RULESYNC_MCP_RELATIVE_FILE_PATH} found.`);
16445
16961
  const factory = this.getFactory(this.toolTarget);
16446
16962
  return await Promise.all([rulesyncMcp].map(async (mcp) => {
16963
+ const targetedRulesyncMcp = mcp.forTarget({
16964
+ toolTarget: this.toolTarget,
16965
+ logger: this.logger
16966
+ });
16447
16967
  const fieldsToStrip = [];
16448
16968
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
16449
16969
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
16450
- const filteredRulesyncMcp = mcp.stripMcpServerFields(fieldsToStrip);
16970
+ const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
16451
16971
  return await factory.class.fromRulesyncMcp({
16452
16972
  outputRoot: this.outputRoot,
16453
16973
  rulesyncMcp: filteredRulesyncMcp,
@@ -16497,6 +17017,25 @@ const PermissionActionSchema = z.enum([
16497
17017
  */
16498
17018
  const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
16499
17019
  /**
17020
+ * Tool-scoped canonical `permission` block: the same shape as the shared
17021
+ * top-level `permission` record, but applied only to the tool whose override
17022
+ * key it appears under. During generation the categories placed here are
17023
+ * merged over the shared `permission` block per category (the tool-scoped
17024
+ * category replaces the shared one wholesale), mirroring how
17025
+ * `.rulesync/hooks.json` merges `{toolname}.hooks` per event.
17026
+ *
17027
+ * @example
17028
+ * { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
17029
+ */
17030
+ const ToolScopedPermissionSchema = z.record(z.string(), PermissionRulesSchema);
17031
+ /**
17032
+ * Generic tool-scoped override block for tools that have no tool-specific
17033
+ * override keys of their own; it carries only the canonical tool-scoped
17034
+ * `permission` block. Kept `looseObject` so future tool-specific keys can be
17035
+ * added without a schema break.
17036
+ */
17037
+ const CanonicalPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
17038
+ /**
16500
17039
  * OpenCode-specific permission value. Unlike the shared canonical block, which
16501
17040
  * only accepts a pattern-to-action map, OpenCode also allows a bare action
16502
17041
  * string that applies to the whole category (e.g. `"external_directory": "deny"`).
@@ -16526,7 +17065,7 @@ const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional
16526
17065
  * @example
16527
17066
  * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
16528
17067
  */
16529
- const HermesPermissionsOverrideSchema = z.looseObject({});
17068
+ const HermesPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
16530
17069
  /**
16531
17070
  * Tool-scoped override block for Cline. Cline's `command-permissions.json`
16532
17071
  * carries a single global `allowRedirects` boolean (gates shell redirection
@@ -16538,7 +17077,10 @@ const HermesPermissionsOverrideSchema = z.looseObject({});
16538
17077
  * @example
16539
17078
  * { "allowRedirects": true }
16540
17079
  */
16541
- const ClinePermissionsOverrideSchema = z.looseObject({ allowRedirects: z.optional(z.boolean()) });
17080
+ const ClinePermissionsOverrideSchema = z.looseObject({
17081
+ permission: z.optional(ToolScopedPermissionSchema),
17082
+ allowRedirects: z.optional(z.boolean())
17083
+ });
16542
17084
  /**
16543
17085
  * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
16544
17086
  * object is a free-form record with tool-specific keys that have no canonical
@@ -16568,7 +17110,10 @@ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
16568
17110
  * @example
16569
17111
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
16570
17112
  */
16571
- const ClaudecodePermissionsOverrideSchema = z.looseObject({ permissions: z.optional(z.looseObject({})) });
17113
+ const ClaudecodePermissionsOverrideSchema = z.looseObject({
17114
+ permission: z.optional(ToolScopedPermissionSchema),
17115
+ permissions: z.optional(z.looseObject({}))
17116
+ });
16572
17117
  /**
16573
17118
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
16574
17119
  * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
@@ -16600,6 +17145,7 @@ const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
16600
17145
  * { "approvalMode": "auto-review" }
16601
17146
  */
16602
17147
  const CursorPermissionsOverrideSchema = z.looseObject({
17148
+ permission: z.optional(ToolScopedPermissionSchema),
16603
17149
  approvalMode: z.optional(z.string()),
16604
17150
  sandbox: z.optional(z.looseObject({}))
16605
17151
  });
@@ -16607,18 +17153,28 @@ const CursorPermissionsOverrideSchema = z.looseObject({
16607
17153
  * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
16608
17154
  * autonomy/sandbox controls with no canonical permission category — under
16609
17155
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
16610
- * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
16611
- * placed here are merged into the matching `settings.json` group and emitted
16612
- * only for Qwen, while the shared `permission` block continues to drive the
16613
- * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
16614
- * passthrough) so any current or future `tools`/`security` key can be authored.
17156
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). It also
17157
+ * exposes `permissions.autoMode` (the Auto Mode classifier config:
17158
+ * `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` see
17159
+ * https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
17160
+ * likewise has no canonical category. Fields placed here are merged into the
17161
+ * matching `settings.json` group and emitted only for Qwen, while the shared
17162
+ * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
17163
+ * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
17164
+ * `tools`/`security`/`autoMode` key can be authored.
16615
17165
  *
16616
17166
  * @example
16617
- * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
17167
+ * {
17168
+ * "tools": { "approvalMode": "auto-edit" },
17169
+ * "security": { "folderTrust": { "enabled": true } },
17170
+ * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
17171
+ * }
16618
17172
  */
16619
17173
  const QwencodePermissionsOverrideSchema = z.looseObject({
17174
+ permission: z.optional(ToolScopedPermissionSchema),
16620
17175
  tools: z.optional(z.looseObject({})),
16621
- security: z.optional(z.looseObject({}))
17176
+ security: z.optional(z.looseObject({})),
17177
+ autoMode: z.optional(z.looseObject({}))
16622
17178
  });
16623
17179
  /**
16624
17180
  * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
@@ -16637,6 +17193,7 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
16637
17193
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
16638
17194
  */
16639
17195
  const ReasonixPermissionsOverrideSchema = z.looseObject({
17196
+ permission: z.optional(ToolScopedPermissionSchema),
16640
17197
  sandbox: z.optional(z.looseObject({})),
16641
17198
  agent: z.optional(z.looseObject({}))
16642
17199
  });
@@ -16645,16 +17202,21 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
16645
17202
  * exposes security controls with no canonical per-command allow/ask/deny slot —
16646
17203
  * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
16647
17204
  * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
16648
- * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
17205
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`,
17206
+ * `mcpAutonomyOverrides` (per-MCP-tool autonomy levels), `enableDroidShield`,
16649
17207
  * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
16650
- * `interactionMode`). Fields placed here are merged into `settings.json` and
17208
+ * `subagentAutonomyLevel`, `interactionMode`). Fields placed here are merged
17209
+ * into `settings.json` and
16651
17210
  * emitted only for Factory Droid, while the shared `permission` block continues
16652
17211
  * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
16653
17212
  *
16654
17213
  * @example
16655
17214
  * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
16656
17215
  */
16657
- const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist: z.optional(z.array(z.string())) });
17216
+ const FactorydroidPermissionsOverrideSchema = z.looseObject({
17217
+ permission: z.optional(ToolScopedPermissionSchema),
17218
+ commandBlocklist: z.optional(z.array(z.string()))
17219
+ });
16658
17220
  /**
16659
17221
  * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
16660
17222
  * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
@@ -16671,6 +17233,7 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist:
16671
17233
  * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
16672
17234
  */
16673
17235
  const WarpPermissionsOverrideSchema = z.looseObject({
17236
+ permission: z.optional(ToolScopedPermissionSchema),
16674
17237
  agent_mode_coding_permissions: z.optional(z.string()),
16675
17238
  agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
16676
17239
  agent_mode_execute_readonly_commands: z.optional(z.boolean())
@@ -16688,6 +17251,7 @@ const WarpPermissionsOverrideSchema = z.looseObject({
16688
17251
  * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16689
17252
  */
16690
17253
  const JuniePermissionsOverrideSchema = z.looseObject({
17254
+ permission: z.optional(ToolScopedPermissionSchema),
16691
17255
  allowReadonlyCommands: z.optional(z.boolean()),
16692
17256
  defaultBehavior: z.optional(z.string())
16693
17257
  });
@@ -16715,6 +17279,7 @@ const JuniePermissionsOverrideSchema = z.looseObject({
16715
17279
  * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16716
17280
  */
16717
17281
  const TaktPermissionsOverrideSchema = z.looseObject({
17282
+ permission: z.optional(ToolScopedPermissionSchema),
16718
17283
  step_permission_overrides: z.optional(z.record(z.string(), z.string())),
16719
17284
  provider_options: z.optional(z.looseObject({}))
16720
17285
  });
@@ -16742,6 +17307,7 @@ const TaktPermissionsOverrideSchema = z.looseObject({
16742
17307
  * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16743
17308
  */
16744
17309
  const AmpPermissionsOverrideSchema = z.looseObject({
17310
+ permission: z.optional(ToolScopedPermissionSchema),
16745
17311
  permissions: z.optional(z.array(z.looseObject({
16746
17312
  tool: z.string(),
16747
17313
  action: z.string()
@@ -16770,6 +17336,7 @@ const AmpPermissionsOverrideSchema = z.looseObject({
16770
17336
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
16771
17337
  */
16772
17338
  const AntigravityCliPermissionsOverrideSchema = z.looseObject({
17339
+ permission: z.optional(ToolScopedPermissionSchema),
16773
17340
  toolPermission: z.optional(z.string()),
16774
17341
  enableTerminalSandbox: z.optional(z.boolean())
16775
17342
  });
@@ -16795,10 +17362,13 @@ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
16795
17362
  * "permission": { "type": "webhook-policy", "webhookUrl": "https://api.example.com/validate" } },
16796
17363
  * { "toolName": "view", "eventType": "tool-response", "permission": { "type": "allow" } } ] }
16797
17364
  */
16798
- const AugmentcodePermissionsOverrideSchema = z.looseObject({ toolPermissions: z.optional(z.array(z.looseObject({
16799
- toolName: z.string(),
16800
- permission: z.looseObject({ type: z.string() })
16801
- }))) });
17365
+ const AugmentcodePermissionsOverrideSchema = z.looseObject({
17366
+ permission: z.optional(ToolScopedPermissionSchema),
17367
+ toolPermissions: z.optional(z.array(z.looseObject({
17368
+ toolName: z.string(),
17369
+ permission: z.looseObject({ type: z.string() })
17370
+ })))
17371
+ });
16802
17372
  /**
16803
17373
  * Tool-scoped override block for Kiro. Kiro's agent config (`.kiro/agents/<name>.json`)
16804
17374
  * exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny
@@ -16824,21 +17394,59 @@ const AugmentcodePermissionsOverrideSchema = z.looseObject({ toolPermissions: z.
16824
17394
  * "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] },
16825
17395
  * "web_fetch": { "trusted": [".*github\\.com.*"] } } }
16826
17396
  */
16827
- const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(z.looseObject({
16828
- shell: z.optional(z.looseObject({
16829
- autoAllowReadonly: z.optional(z.boolean()),
16830
- denyByDefault: z.optional(z.boolean())
16831
- })),
16832
- aws: z.optional(z.looseObject({
16833
- allowedServices: z.optional(z.array(z.string())),
16834
- deniedServices: z.optional(z.array(z.string())),
16835
- autoAllowReadonly: z.optional(z.boolean())
16836
- })),
16837
- web_fetch: z.optional(z.looseObject({
16838
- trusted: z.optional(z.array(z.string())),
16839
- blocked: z.optional(z.array(z.string()))
17397
+ const KiroPermissionsOverrideSchema = z.looseObject({
17398
+ permission: z.optional(ToolScopedPermissionSchema),
17399
+ toolsSettings: z.optional(z.looseObject({
17400
+ shell: z.optional(z.looseObject({
17401
+ autoAllowReadonly: z.optional(z.boolean()),
17402
+ denyByDefault: z.optional(z.boolean())
17403
+ })),
17404
+ aws: z.optional(z.looseObject({
17405
+ allowedServices: z.optional(z.array(z.string())),
17406
+ deniedServices: z.optional(z.array(z.string())),
17407
+ autoAllowReadonly: z.optional(z.boolean())
17408
+ })),
17409
+ web_fetch: z.optional(z.looseObject({
17410
+ trusted: z.optional(z.array(z.string())),
17411
+ blocked: z.optional(z.array(z.string()))
17412
+ }))
16840
17413
  }))
16841
- })) });
17414
+ });
17415
+ /**
17416
+ * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
17417
+ * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
17418
+ * Codex still accepts, so it is included so existing configs round-trip. The
17419
+ * granular table form (`{ granular = { … } }`) is modeled separately in the
17420
+ * override union.
17421
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17422
+ */
17423
+ const CodexApprovalPolicySchema = z.enum([
17424
+ "untrusted",
17425
+ "on-request",
17426
+ "on-failure",
17427
+ "never"
17428
+ ]);
17429
+ /**
17430
+ * Codex CLI's classic sandbox mode. Serialized as a kebab-case string in
17431
+ * `.codex/config.toml`.
17432
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17433
+ */
17434
+ const CodexSandboxModeSchema = z.enum([
17435
+ "read-only",
17436
+ "workspace-write",
17437
+ "danger-full-access"
17438
+ ]);
17439
+ /**
17440
+ * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
17441
+ * value Codex still accepts for backward compatibility, so it is included so
17442
+ * existing configs round-trip through the rulesync model.
17443
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17444
+ */
17445
+ const CodexApprovalsReviewerSchema = z.enum([
17446
+ "user",
17447
+ "auto_review",
17448
+ "guardian_subagent"
17449
+ ]);
16842
17450
  /**
16843
17451
  * Codex CLI-scoped permission override.
16844
17452
  *
@@ -16848,15 +17456,16 @@ const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(
16848
17456
  * override whose fields are written verbatim as top-level `.codex/config.toml`
16849
17457
  * keys (the override wins per key; existing sibling keys the user set directly
16850
17458
  * are preserved):
16851
- * - `approval_policy` — `untrusted` | `on-request` | `never`, or a
16852
- * `{ granular = { … } }` table (kept verbatim; the granular schema has
16853
- * required fields that are brittle to model as typed keys).
17459
+ * - `approval_policy` — `untrusted` | `on-request` (legacy alias `on-failure`) |
17460
+ * `never`, or a `{ granular = { … } }` table (kept verbatim; the granular
17461
+ * schema has required fields that are brittle to model as typed keys).
16854
17462
  * - `sandbox_mode` — `read-only` | `workspace-write` | `danger-full-access`,
16855
17463
  * with the sibling `sandbox_workspace_write` table (`network_access`,
16856
17464
  * `writable_roots`, …).
16857
17465
  * - `apps` — per-app tool gating (`apps.<id>.tools.<tool>.approval_mode` /
16858
17466
  * `.enabled`, `apps.<id>.default_tools_approval_mode`).
16859
- * - `approvals_reviewer` — the reviewer-approval surface.
17467
+ * - `approvals_reviewer` — the reviewer-approval surface (`user` | `auto_review`
17468
+ * | `guardian_subagent`), or a table for the richer reviewer config.
16860
17469
  *
16861
17470
  * Two surfaces are deliberately NOT authorable here so the override can never
16862
17471
  * clobber a feature-owned key: `mcp_servers.*` per-MCP gating is owned by the
@@ -16874,11 +17483,12 @@ const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(
16874
17483
  * "sandbox_workspace_write": { "network_access": true } }
16875
17484
  */
16876
17485
  const CodexcliPermissionsOverrideSchema = z.looseObject({
16877
- approval_policy: z.optional(z.union([z.string(), z.looseObject({})])),
16878
- sandbox_mode: z.optional(z.string()),
17486
+ permission: z.optional(ToolScopedPermissionSchema),
17487
+ approval_policy: z.optional(z.union([CodexApprovalPolicySchema, z.looseObject({})])),
17488
+ sandbox_mode: z.optional(CodexSandboxModeSchema),
16879
17489
  sandbox_workspace_write: z.optional(z.looseObject({})),
16880
17490
  apps: z.optional(z.looseObject({})),
16881
- approvals_reviewer: z.optional(z.union([z.string(), z.looseObject({})]))
17491
+ approvals_reviewer: z.optional(z.union([CodexApprovalsReviewerSchema, z.looseObject({})]))
16882
17492
  });
16883
17493
  /**
16884
17494
  * Permissions configuration.
@@ -16892,6 +17502,15 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
16892
17502
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
16893
17503
  * block and ignores them.
16894
17504
  *
17505
+ * Additionally, every permissions-capable tool accepts a canonical tool-scoped
17506
+ * `permission` block under its override key (`{toolname}.permission`, same
17507
+ * shape as the shared `permission` record). Its categories apply only to that
17508
+ * tool, merged over the shared block per category — see
17509
+ * `RulesyncPermissions.forTarget`. `kiro-cli`/`kiro-ide` alias to the `kiro`
17510
+ * key and `hermesagent` to `hermes` (matching the shared output file each
17511
+ * writes). OpenCode/Kilo/Vibe keep their existing tool-native `permission`
17512
+ * override semantics (handled by their translators, not the central merge).
17513
+ *
16895
17514
  * @example
16896
17515
  * {
16897
17516
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
@@ -16917,7 +17536,13 @@ const PermissionsConfigSchema = z.looseObject({
16917
17536
  "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema),
16918
17537
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
16919
17538
  kiro: z.optional(KiroPermissionsOverrideSchema),
16920
- codexcli: z.optional(CodexcliPermissionsOverrideSchema)
17539
+ codexcli: z.optional(CodexcliPermissionsOverrideSchema),
17540
+ "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
17541
+ devin: z.optional(CanonicalPermissionsOverrideSchema),
17542
+ goose: z.optional(CanonicalPermissionsOverrideSchema),
17543
+ grokcli: z.optional(CanonicalPermissionsOverrideSchema),
17544
+ rovodev: z.optional(CanonicalPermissionsOverrideSchema),
17545
+ zed: z.optional(CanonicalPermissionsOverrideSchema)
16921
17546
  });
16922
17547
  /**
16923
17548
  * Full permissions file schema including optional $schema field.
@@ -16932,7 +17557,7 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
16932
17557
  json;
16933
17558
  constructor(params) {
16934
17559
  super({ ...params });
16935
- this.json = JSON.parse(this.fileContent);
17560
+ this.json = parseJsonc(this.fileContent);
16936
17561
  if (params.validate) {
16937
17562
  const result = this.validate();
16938
17563
  if (!result.success) throw result.error;
@@ -16941,7 +17566,11 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
16941
17566
  static getSettablePaths() {
16942
17567
  return {
16943
17568
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
16944
- relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME
17569
+ relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
17570
+ jsonc: {
17571
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
17572
+ relativeFilePath: RULESYNC_PERMISSIONS_JSONC_FILE_NAME
17573
+ }
16945
17574
  };
16946
17575
  }
16947
17576
  validate() {
@@ -16957,20 +17586,85 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
16957
17586
  }
16958
17587
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
16959
17588
  const paths = RulesyncPermissions.getSettablePaths();
16960
- const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16961
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
16962
- const fileContent = await readFileContent(filePath);
16963
- return new RulesyncPermissions({
16964
- outputRoot,
17589
+ const candidates = [paths.jsonc, {
16965
17590
  relativeDirPath: paths.relativeDirPath,
16966
- relativeFilePath: paths.relativeFilePath,
16967
- fileContent,
16968
- validate
16969
- });
17591
+ relativeFilePath: paths.relativeFilePath
17592
+ }];
17593
+ for (const candidate of candidates) {
17594
+ const filePath = join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
17595
+ if (!await fileExists(filePath)) continue;
17596
+ const fileContent = await readFileContent(filePath);
17597
+ return new RulesyncPermissions({
17598
+ outputRoot,
17599
+ relativeDirPath: candidate.relativeDirPath,
17600
+ relativeFilePath: candidate.relativeFilePath,
17601
+ fileContent,
17602
+ validate
17603
+ });
17604
+ }
17605
+ throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} or ${RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH} found.`);
16970
17606
  }
16971
17607
  getJson() {
16972
17608
  return this.json;
16973
17609
  }
17610
+ /**
17611
+ * Build the effective permissions config for one tool target by merging the
17612
+ * tool-scoped `{toolname}.permission` block over the shared `permission`
17613
+ * block, per category (the tool-scoped category replaces the shared one
17614
+ * wholesale — mirroring how `{toolname}.hooks` merges per event in
17615
+ * `.rulesync/hooks.json`). The consumed `permission` key is stripped from
17616
+ * the override block so verbatim-passthrough translators (e.g. the Hermes
17617
+ * deep merge or the Codex CLI top-level key whitelist) never see it.
17618
+ *
17619
+ * Returns the same instance when the target has no tool-scoped canonical
17620
+ * `permission` block, or when the target's translator natively consumes its
17621
+ * own `permission` override shape (OpenCode / Kilo / Vibe).
17622
+ */
17623
+ forTarget({ toolTarget, logger }) {
17624
+ if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
17625
+ const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
17626
+ const json = this.json;
17627
+ 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).`);
17628
+ const overrideBlock = json[overrideKey];
17629
+ if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
17630
+ const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
17631
+ const merged = {
17632
+ ...json,
17633
+ permission: {
17634
+ ...this.json.permission,
17635
+ ...toolScopedPermission
17636
+ }
17637
+ };
17638
+ if (Object.keys(restOverride).length > 0) merged[overrideKey] = restOverride;
17639
+ else delete merged[overrideKey];
17640
+ return new RulesyncPermissions({
17641
+ outputRoot: this.outputRoot,
17642
+ relativeDirPath: this.relativeDirPath,
17643
+ relativeFilePath: this.relativeFilePath,
17644
+ fileContent: JSON.stringify(merged, null, 2)
17645
+ });
17646
+ }
17647
+ };
17648
+ /**
17649
+ * Targets whose tool-scoped `permission` override uses tool-native semantics
17650
+ * consumed directly by their translator (bare action strings / tool-only
17651
+ * categories for OpenCode and Kilo, `sensitive_patterns` objects for Vibe).
17652
+ * The central canonical merge must not touch those blocks.
17653
+ */
17654
+ const NATIVE_PERMISSION_OVERRIDE_TARGETS = /* @__PURE__ */ new Set([
17655
+ "opencode",
17656
+ "kilo",
17657
+ "vibe"
17658
+ ]);
17659
+ /**
17660
+ * Targets that read their tool-scoped override from a differently named key:
17661
+ * Kiro IDE/CLI share the `kiro` block (they write the same agent config) and
17662
+ * Hermes Agent's established override key is `hermes`.
17663
+ */
17664
+ const PERMISSION_OVERRIDE_KEY_ALIASES = {
17665
+ "kiro-cli": "kiro",
17666
+ "kiro-ide": "kiro",
17667
+ hermesagent: "hermes"
16974
17668
  };
16975
17669
  //#endregion
16976
17670
  //#region src/features/permissions/tool-permissions.ts
@@ -18810,10 +19504,15 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18810
19504
  existingDomainsHadUnknown,
18811
19505
  logger
18812
19506
  });
18813
- permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
19507
+ const profile = mergeWithExistingProfile({
18814
19508
  newProfile,
18815
19509
  existingProfile
18816
19510
  });
19511
+ permissionsTable[RULESYNC_PROFILE_NAME] = preserveUnmanagedProfileKeys({
19512
+ rawExistingProfile: permissionsTable[RULESYNC_PROFILE_NAME],
19513
+ profile,
19514
+ logger
19515
+ });
18817
19516
  const overridePatch = computeCodexcliOverridePatch({
18818
19517
  existing,
18819
19518
  override: rulesyncPermissions.getJson().codexcli,
@@ -19009,6 +19708,43 @@ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingD
19009
19708
  if (existingProfile?.network?.mode !== void 0) logger?.warn(`Preserving existing "network.mode" from config. Review this value manually as it may grant broader network access than the Rulesync-managed domain rules.`);
19010
19709
  if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19011
19710
  }
19711
+ const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
19712
+ "description",
19713
+ "extends",
19714
+ "filesystem",
19715
+ "network"
19716
+ ]);
19717
+ const MANAGED_NETWORK_KEYS = /* @__PURE__ */ new Set([
19718
+ "enabled",
19719
+ "mode",
19720
+ "domains",
19721
+ "unix_sockets"
19722
+ ]);
19723
+ /**
19724
+ * Re-attach the keys of the existing rulesync profile that rulesync does not
19725
+ * manage. The managed keys always come from `profile` (the freshly computed
19726
+ * merge result); unmanaged siblings — at the profile level and inside the
19727
+ * `network` table — are preserved verbatim from the existing config so a
19728
+ * regeneration never deletes user-authored Codex settings.
19729
+ */
19730
+ function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
19731
+ const rawProfileTable = toMutableTable(rawExistingProfile);
19732
+ const profileExtras = Object.fromEntries(Object.entries(rawProfileTable).filter(([key]) => !MANAGED_PROFILE_KEYS.has(key)));
19733
+ const rawNetworkTable = toMutableTable(rawProfileTable.network);
19734
+ const networkExtras = Object.fromEntries(Object.entries(rawNetworkTable).filter(([key]) => !MANAGED_NETWORK_KEYS.has(key)));
19735
+ const preservedKeyNames = [...Object.keys(profileExtras), ...Object.keys(networkExtras).map((key) => `network.${key}`)];
19736
+ if (preservedKeyNames.length > 0) logger?.warn(`Preserving unmanaged keys in the "${RULESYNC_PROFILE_NAME}" permissions profile: ${preservedKeyNames.join(", ")}. Review them manually; rulesync carries them forward verbatim.`);
19737
+ const result = {
19738
+ ...profileExtras,
19739
+ ...profile
19740
+ };
19741
+ const mergedNetwork = {
19742
+ ...networkExtras,
19743
+ ...profile.network
19744
+ };
19745
+ if (Object.keys(mergedNetwork).length > 0) result.network = mergedNetwork;
19746
+ return result;
19747
+ }
19012
19748
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
19013
19749
  if (!existingProfile) return newProfile;
19014
19750
  const mergedNetwork = { ...newProfile.network };
@@ -19739,9 +20475,11 @@ const FACTORYDROID_OVERRIDE_KEYS = [
19739
20475
  "networkPolicy",
19740
20476
  "sandbox",
19741
20477
  "mcpPolicy",
20478
+ "mcpAutonomyOverrides",
19742
20479
  "enableDroidShield",
19743
20480
  "sessionDefaultSettings",
19744
20481
  "maxAutonomyLevel",
20482
+ "subagentAutonomyLevel",
19745
20483
  "interactionMode"
19746
20484
  ];
19747
20485
  /**
@@ -20099,7 +20837,6 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
20099
20837
  }
20100
20838
  //#endregion
20101
20839
  //#region src/features/permissions/grokcli-permissions.ts
20102
- const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
20103
20840
  const GROKCLI_UI_KEY = "ui";
20104
20841
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
20105
20842
  const GROKCLI_PERMISSION_KEY = "permission";
@@ -20201,13 +20938,19 @@ function parseGrokEntry(entry) {
20201
20938
  * compatibility with older Grok versions: `always-approve` when the config is
20202
20939
  * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
20203
20940
  * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
20204
- * arrays).
20205
- *
20206
- * This surface is **global only** — the adapter syncs the user-level
20207
- * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
20208
- * file; project scope is not modeled here.) The shared config is merged in
20941
+ * arrays). It is a user-level UI setting, so it is only written in global scope
20942
+ * (see below).
20943
+ *
20944
+ * Both scopes are supported: the adapter syncs the project-level
20945
+ * `./.grok/config.toml` (default) and the user-level `~/.grok/config.toml`
20946
+ * (with `--global`). Grok documents that "Project configs are limited to MCP
20947
+ * servers, plugins, and permission rules, not full user configs"
20948
+ * (https://docs.x.ai/build/settings), so the project config carries only the
20949
+ * fine-grained `[permission]` rules; the coarse `[ui] permission_mode` UI toggle
20950
+ * is written in global scope only. The shared config is merged in
20209
20951
  * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
20210
- * tools it models and the `[ui] permission_mode` value, while every other key
20952
+ * tools it models and (in global scope) the `[ui] permission_mode` value, while
20953
+ * every other key
20211
20954
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
20212
20955
  * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
20213
20956
  * file is never deleted.
@@ -20229,7 +20972,6 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20229
20972
  };
20230
20973
  }
20231
20974
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20232
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
20233
20975
  const paths = GrokcliPermissions.getSettablePaths({ global });
20234
20976
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
20235
20977
  return new GrokcliPermissions({
@@ -20238,11 +20980,10 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20238
20980
  relativeFilePath: paths.relativeFilePath,
20239
20981
  fileContent,
20240
20982
  validate,
20241
- global: true
20983
+ global
20242
20984
  });
20243
20985
  }
20244
20986
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
20245
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
20246
20987
  const paths = GrokcliPermissions.getSettablePaths({ global });
20247
20988
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20248
20989
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -20261,11 +21002,10 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20261
21002
  deny: buckets.deny,
20262
21003
  ask: buckets.ask
20263
21004
  };
20264
- const mode = deriveGrokPermissionMode(config);
20265
- const ui = {
21005
+ const uiPatch = global ? { [GROKCLI_UI_KEY]: {
20266
21006
  ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
20267
- [GROKCLI_PERMISSION_MODE_KEY]: mode
20268
- };
21007
+ [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
21008
+ } } : {};
20269
21009
  return new GrokcliPermissions({
20270
21010
  outputRoot,
20271
21011
  relativeDirPath: paths.relativeDirPath,
@@ -20276,12 +21016,12 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20276
21016
  existingContent,
20277
21017
  patch: {
20278
21018
  [GROKCLI_PERMISSION_KEY]: permission,
20279
- [GROKCLI_UI_KEY]: ui
21019
+ ...uiPatch
20280
21020
  },
20281
21021
  filePath
20282
21022
  }),
20283
21023
  validate: true,
20284
- global: true
21024
+ global
20285
21025
  });
20286
21026
  }
20287
21027
  toRulesyncPermissions() {
@@ -20302,14 +21042,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20302
21042
  error: null
20303
21043
  };
20304
21044
  }
20305
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
21045
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
20306
21046
  return new GrokcliPermissions({
20307
21047
  outputRoot,
20308
21048
  relativeDirPath,
20309
21049
  relativeFilePath,
20310
21050
  fileContent: "",
20311
21051
  validate: false,
20312
- global: true
21052
+ global
20313
21053
  });
20314
21054
  }
20315
21055
  };
@@ -21224,6 +21964,22 @@ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
21224
21964
  "notebookedit",
21225
21965
  "agent"
21226
21966
  ]);
21967
+ /**
21968
+ * Translate between rulesync's canonical permission category names and
21969
+ * OpenCode's native permission keys. OpenCode has no `agent` key — subagent
21970
+ * launches are gated by the `task` key (see the documented key list at
21971
+ * https://opencode.ai/docs/permissions/). Without this translation a canonical
21972
+ * `agent: deny` would be written verbatim into `opencode.json` and silently
21973
+ * ignored by OpenCode. Unknown names pass through unchanged.
21974
+ */
21975
+ const CANONICAL_TO_OPENCODE_PERMISSION_KEYS = { agent: "task" };
21976
+ const OPENCODE_TO_CANONICAL_PERMISSION_KEYS = Object.fromEntries(Object.entries(CANONICAL_TO_OPENCODE_PERMISSION_KEYS).map(([canonical, opencode]) => [opencode, canonical]));
21977
+ function toOpencodePermissionKey(canonical) {
21978
+ return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? canonical;
21979
+ }
21980
+ function toCanonicalPermissionKey(opencodeKey) {
21981
+ return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
21982
+ }
21227
21983
  function isSharedPermissionCategory(category) {
21228
21984
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
21229
21985
  }
@@ -21290,6 +22046,8 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
21290
22046
  }
21291
22047
  const rulesyncJson = rulesyncPermissions.getJson();
21292
22048
  const overridePermission = rulesyncJson.opencode?.permission ?? {};
22049
+ const sharedPermission = {};
22050
+ for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
21293
22051
  return new OpencodePermissions({
21294
22052
  outputRoot,
21295
22053
  relativeDirPath: basePaths.relativeDirPath,
@@ -21299,7 +22057,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
21299
22057
  feature: "permissions",
21300
22058
  existingContent: fileContent ?? "",
21301
22059
  patch: { permission: {
21302
- ...rulesyncJson.permission,
22060
+ ...sharedPermission,
21303
22061
  ...overridePermission
21304
22062
  } },
21305
22063
  filePath: join(jsonDir, relativeFilePath)
@@ -21315,8 +22073,11 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
21315
22073
  }
21316
22074
  const shared = {};
21317
22075
  const overrideOnly = {};
21318
- for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
21319
- else overrideOnly[category] = value;
22076
+ for (const [category, value] of Object.entries(rawPermission)) {
22077
+ const canonicalCategory = toCanonicalPermissionKey(category);
22078
+ if (isSharedPermissionCategory(canonicalCategory)) shared[canonicalCategory] = typeof value === "string" ? { "*": value } : value;
22079
+ else overrideOnly[category] = value;
22080
+ }
21320
22081
  const json = Object.keys(overrideOnly).length > 0 ? {
21321
22082
  permission: shared,
21322
22083
  opencode: { permission: overrideOnly }
@@ -21441,6 +22202,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
21441
22202
  "disabled"
21442
22203
  ];
21443
22204
  const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
22205
+ const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
21444
22206
  function asPlainRecord(value) {
21445
22207
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
21446
22208
  }
@@ -21512,8 +22274,9 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21512
22274
  else delete mergedPermissions.ask;
21513
22275
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21514
22276
  else delete mergedPermissions.deny;
21515
- const patch = { permissions: mergedPermissions };
21516
22277
  const override = config.qwencode;
22278
+ if (override?.autoMode !== void 0) mergedPermissions.autoMode = override.autoMode;
22279
+ const patch = { permissions: mergedPermissions };
21517
22280
  if (override?.tools !== void 0) patch.tools = {
21518
22281
  ...asPlainRecord(settings.tools),
21519
22282
  ...asPlainRecord(override.tools)
@@ -21555,9 +22318,11 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21555
22318
  });
21556
22319
  const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
21557
22320
  const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
22321
+ const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
21558
22322
  const qwencodeOverride = {};
21559
22323
  if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
21560
22324
  if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
22325
+ if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
21561
22326
  const result = { ...config };
21562
22327
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
21563
22328
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -23066,7 +23831,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
23066
23831
  ["grokcli", {
23067
23832
  class: GrokcliPermissions,
23068
23833
  meta: {
23069
- supportsProject: false,
23834
+ supportsProject: true,
23070
23835
  supportsGlobal: true,
23071
23836
  supportsImport: true
23072
23837
  }
@@ -23241,16 +24006,20 @@ var PermissionsProcessor = class extends FeatureProcessor {
23241
24006
  if (!rulesyncPermissions) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
23242
24007
  const factory = toolPermissionsFactories.get(this.toolTarget);
23243
24008
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
24009
+ const effectivePermissions = rulesyncPermissions.forTarget({
24010
+ toolTarget: this.toolTarget,
24011
+ logger: this.logger
24012
+ });
23244
24013
  const toolPermissions = await factory.class.fromRulesyncPermissions({
23245
24014
  outputRoot: this.outputRoot,
23246
- rulesyncPermissions,
24015
+ rulesyncPermissions: effectivePermissions,
23247
24016
  logger: this.logger,
23248
24017
  global: this.global
23249
24018
  });
23250
24019
  if (this.toolTarget !== "codexcli") return [toolPermissions];
23251
24020
  return [toolPermissions, createCodexcliBashRulesFile({
23252
24021
  outputRoot: this.outputRoot,
23253
- config: rulesyncPermissions.getJson()
24022
+ config: effectivePermissions.getJson()
23254
24023
  })];
23255
24024
  }
23256
24025
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -39346,6 +40115,16 @@ async function generatePermissionsCore(params) {
39346
40115
  //#endregion
39347
40116
  //#region src/lib/import.ts
39348
40117
  /**
40118
+ * Import always writes the `.json` variant of a fixed-path source file, but a
40119
+ * sibling `.jsonc` variant takes precedence at read time — so an import into a
40120
+ * project that authors the `.jsonc` variant would be silently shadowed. Warn
40121
+ * so the user merges (or removes) one of the two by hand.
40122
+ */
40123
+ async function warnIfShadowedByJsonc(params) {
40124
+ const { outputRoot, jsonRelativePath, jsoncRelativePath, logger } = params;
40125
+ if (await fileExists(join(outputRoot, jsoncRelativePath))) logger.warn(`${jsoncRelativePath} exists and takes precedence over the imported ${jsonRelativePath}. Merge the imported content into ${jsoncRelativePath} (or remove it) so the import takes effect.`);
40126
+ }
40127
+ /**
39349
40128
  * Import configuration files from AI tools.
39350
40129
  */
39351
40130
  async function importFromTool(params) {
@@ -39457,6 +40236,12 @@ async function importMcpCore(params) {
39457
40236
  }
39458
40237
  const rulesyncFiles = await mcpProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39459
40238
  const { count: writtenCount } = await mcpProcessor.writeAiFiles(rulesyncFiles);
40239
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40240
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40241
+ jsonRelativePath: RULESYNC_MCP_RELATIVE_FILE_PATH,
40242
+ jsoncRelativePath: RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH,
40243
+ logger
40244
+ });
39460
40245
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} MCP files`);
39461
40246
  return writtenCount;
39462
40247
  }
@@ -39556,6 +40341,12 @@ async function importHooksCore(params) {
39556
40341
  }
39557
40342
  const rulesyncFiles = await hooksProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39558
40343
  const { count: writtenCount } = await hooksProcessor.writeAiFiles(rulesyncFiles);
40344
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40345
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40346
+ jsonRelativePath: RULESYNC_HOOKS_RELATIVE_FILE_PATH,
40347
+ jsoncRelativePath: RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH,
40348
+ logger
40349
+ });
39559
40350
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} hooks file(s)`);
39560
40351
  return writtenCount;
39561
40352
  }
@@ -39585,10 +40376,16 @@ async function importPermissionsCore(params) {
39585
40376
  }
39586
40377
  const rulesyncFiles = await permissionsProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39587
40378
  const { count: writtenCount } = await permissionsProcessor.writeAiFiles(rulesyncFiles);
40379
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40380
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40381
+ jsonRelativePath: RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH,
40382
+ jsoncRelativePath: RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH,
40383
+ logger
40384
+ });
39588
40385
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} permissions file(s)`);
39589
40386
  return writtenCount;
39590
40387
  }
39591
40388
  //#endregion
39592
- export { removeTempDirectory as $, RulesyncCommand as A, checkPathTraversal as B, RulesyncHooks as C, RULESYNC_RULES_RELATIVE_DIR_PATH as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, formatError as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Et, findControlCharacter as F, findFilesByGlobs as G, directoryExists as H, ConsoleLogger as I, isSymlink as J, getFileSize as K, JsonLogger as L, stringifyFrontmatter as M, loadYaml as N, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES as Ot, ConfigResolver as P, removeFile as Q, CLIError as R, HooksProcessor as S, RULESYNC_RELATIVE_DIR_PATH as St, CLAUDECODE_DIR as T, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Tt, ensureDir as U, createTempDirectory as V, fileExists as W, readFileContent as X, listDirectoryFiles as Y, removeDirectory as Z, RulesyncPermissions as _, RULESYNC_MCP_RELATIVE_FILE_PATH as _t, convertFromTool as a, MAX_FILE_SIZE as at, IgnoreProcessor as b, RULESYNC_PERMISSIONS_FILE_NAME as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dt, toPosixPath as et, SkillsProcessor as f, RULESYNC_HOOKS_FILE_NAME as ft, SKILL_FILE_NAME as g, RULESYNC_MCP_FILE_NAME as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as ht, getProcessorRegistryEntry as i, ToolTargetSchema as it, RulesyncCommandFrontmatterSchema as j, CLAUDECODE_SKILLS_DIR_PATH as k, ALL_FEATURES_WITH_WILDCARD as kt, SubagentsProcessor as l, RULESYNC_CONFIG_RELATIVE_FILE_PATH as lt, RulesyncSkill as m, RULESYNC_IGNORE_RELATIVE_FILE_PATH as mt, checkRulesyncDirExists as n, ALL_TOOL_TARGETS as nt, RulesProcessor as o, RULESYNC_AIIGNORE_FILE_NAME as ot, getLocalSkillDirNames as p, RULESYNC_HOOKS_RELATIVE_FILE_PATH as pt, getHomeDirectory as q, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rt, RulesyncRule as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as st, importFromTool as t, writeFileContent as tt, RulesyncSubagent as u, RULESYNC_CONFIG_SCHEMA_URL as ut, McpProcessor as v, RULESYNC_MCP_SCHEMA_URL as vt, CommandsProcessor as w, RULESYNC_SKILLS_RELATIVE_DIR_PATH as wt, RulesyncIgnore as x, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as xt, RulesyncMcp as y, RULESYNC_OVERVIEW_FILE_NAME as yt, ErrorCodes as z };
40389
+ export { removeFile as $, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as At, ErrorCodes as B, RulesyncHooks as C, RULESYNC_PERMISSIONS_FILE_NAME as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_RELATIVE_DIR_PATH as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_SCHEMA_URL as Et, ConfigResolver as F, fileExists as G, createTempDirectory as H, findControlCharacter as I, getHomeDirectory as J, findFilesByGlobs as K, ConsoleLogger as L, RulesyncCommandFrontmatterSchema as M, formatError as Mt, stringifyFrontmatter as N, ALL_FEATURES as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RULES_RELATIVE_DIR_PATH as Ot, loadYaml as P, ALL_FEATURES_WITH_WILDCARD as Pt, removeDirectory as Q, JsonLogger as R, HooksProcessor as S, RULESYNC_OVERVIEW_FILE_NAME as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Tt, directoryExists as U, checkPathTraversal as V, ensureDir as W, listDirectoryFiles as X, isSymlink as Y, readFileContent as Z, RulesyncPermissions as _, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as _t, convertFromTool as a, ToolTargetSchema as at, IgnoreProcessor as b, RULESYNC_MCP_RELATIVE_FILE_PATH as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_CONFIG_SCHEMA_URL as dt, removeTempDirectory as et, SkillsProcessor as f, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ft, SKILL_FILE_NAME as g, RULESYNC_IGNORE_RELATIVE_FILE_PATH as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_HOOKS_RELATIVE_FILE_PATH as ht, getProcessorRegistryEntry as i, ALL_TOOL_TARGETS_WITH_WILDCARD as it, RulesyncCommand as j, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_SKILLS_RELATIVE_DIR_PATH as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as lt, RulesyncSkill as m, RULESYNC_HOOKS_JSONC_FILE_NAME as mt, checkRulesyncDirExists as n, writeFileContent as nt, RulesProcessor as o, MAX_FILE_SIZE as ot, getLocalSkillDirNames as p, RULESYNC_HOOKS_FILE_NAME as pt, getFileSize as q, generate as r, ALL_TOOL_TARGETS as rt, RulesyncRule as s, RULESYNC_AIIGNORE_FILE_NAME as st, importFromTool as t, toPosixPath as tt, RulesyncSubagent as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ut, McpProcessor as v, RULESYNC_MCP_FILE_NAME as vt, CommandsProcessor as w, RULESYNC_PERMISSIONS_JSONC_FILE_NAME as wt, RulesyncIgnore as x, RULESYNC_MCP_SCHEMA_URL as xt, RulesyncMcp as y, RULESYNC_MCP_JSONC_FILE_NAME as yt, CLIError as z };
39593
40390
 
39594
- //# sourceMappingURL=import-DC9T1JlQ.js.map
40391
+ //# sourceMappingURL=import-B02AyWkQ.js.map