rulesync 43.4.2 → 43.6.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.
@@ -873,6 +873,7 @@ const hooksProcessorToolTargetTuple = [
873
873
  "qwencode",
874
874
  "reasonix",
875
875
  "tabnine",
876
+ "trae",
876
877
  "grokcli",
877
878
  "gitlabduo",
878
879
  "lettacode",
@@ -3923,6 +3924,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3923
3924
  ".poolside/settings.yaml",
3924
3925
  ".config/poolside/settings.yaml",
3925
3926
  ".tabnine/agent/settings.json",
3927
+ ".trae/hooks.json",
3926
3928
  ".vibe/config.toml",
3927
3929
  ".zcode/config.json",
3928
3930
  ".zcode/cli/config.json",
@@ -6005,6 +6007,34 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
6005
6007
  };
6006
6008
  const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CORTEXCODE_EVENT_NAMES);
6007
6009
  /**
6010
+ * Hook events supported by TRAE.
6011
+ *
6012
+ * TRAE reads `.trae/hooks.json` (project) and `~/.trae/hooks.json` (user):
6013
+ * `{ "version"?: 1, "hooks": { "<Event>": [{ "matcher"?, "hooks": [{ "type":
6014
+ * "command", "command", "timeout"? }] }] } }` with Claude-Code-style
6015
+ * PascalCase event names. `$TRAE_PROJECT_DIR` resolves to the workspace root
6016
+ * inside commands.
6017
+ *
6018
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
6019
+ */
6020
+ const TRAE_HOOK_EVENTS = [
6021
+ "sessionStart",
6022
+ "beforeSubmitPrompt",
6023
+ "preToolUse",
6024
+ "postToolUse",
6025
+ "stop",
6026
+ "notification"
6027
+ ];
6028
+ const CANONICAL_TO_TRAE_EVENT_NAMES = {
6029
+ sessionStart: "SessionStart",
6030
+ beforeSubmitPrompt: "UserPromptSubmit",
6031
+ preToolUse: "PreToolUse",
6032
+ postToolUse: "PostToolUse",
6033
+ stop: "Stop",
6034
+ notification: "Notification"
6035
+ };
6036
+ const TRAE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_TRAE_EVENT_NAMES);
6037
+ /**
6008
6038
  * Hook events supported by CodeBuddy Code.
6009
6039
  *
6010
6040
  * CodeBuddy Code reads hooks from the `hooks` key of `.codebuddy/settings.json`
@@ -6477,6 +6507,7 @@ const HooksConfigSchema = z.looseObject({
6477
6507
  bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6478
6508
  codebuddy: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6479
6509
  cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6510
+ trae: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6480
6511
  commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6481
6512
  lettacode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6482
6513
  continue: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
@@ -8378,17 +8409,29 @@ const CodewhalePermissionsOverrideSchema = z.looseObject({
8378
8409
  rules: z.optional(z.array(CodewhalePermissionRuleSchema))
8379
8410
  });
8380
8411
  /**
8381
- * The starting permission level VS Code Copilot Chat gives new chat sessions
8382
- * (`chat.permissions.default`). Values are VS Code's own: `default` (Default
8412
+ * The values VS Code's `chat.permissions.default` setting accepts: the starting
8413
+ * permission level of new Local-harness chat sessions — `default` (Default
8383
8414
  * Permissions), `autoApprove` (Bypass Approvals) and `autopilot` (Autopilot).
8384
8415
  *
8385
8416
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
8386
8417
  */
8387
- const COPILOT_DEFAULT_PERMISSION_MODES = [
8418
+ const COPILOT_LOCAL_PERMISSION_LEVELS = [
8388
8419
  "default",
8389
8420
  "autoApprove",
8390
8421
  "autopilot"
8391
8422
  ];
8423
+ /**
8424
+ * The starting permission level the `copilot.defaultPermissionMode` override
8425
+ * sets for new chat sessions: the {@link COPILOT_LOCAL_PERMISSION_LEVELS}, plus
8426
+ * `assisted` (Assisted permissions, GA for Copilot-harness sessions since VS
8427
+ * Code 1.144 and unavailable to Local sessions). Each value is written to both
8428
+ * `chat.permissions.default` (Local sessions) and the `approvals` / `mode` of
8429
+ * `chat.defaultConfiguration` (Copilot-harness sessions, VS Code 1.144+).
8430
+ *
8431
+ * @see https://code.visualstudio.com/updates/v1_144
8432
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
8433
+ */
8434
+ const COPILOT_DEFAULT_PERMISSION_MODES = [...COPILOT_LOCAL_PERMISSION_LEVELS, "assisted"];
8392
8435
  /** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
8393
8436
  const CopilotSandboxEnabledSchema = z.enum(["off", "on"]);
8394
8437
  /** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
@@ -8429,7 +8472,7 @@ const CopilotSandboxSchema = z.strictObject({
8429
8472
  /**
8430
8473
  * Tool-scoped override block for GitHub Copilot in VS Code.
8431
8474
  * `defaultPermissionMode` writes the workspace `chat.permissions.default`
8432
- * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
8475
+ * and `chat.defaultConfiguration` settings, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
8433
8476
  * has a canonical category.
8434
8477
  */
8435
8478
  const CopilotPermissionsOverrideSchema = z.looseObject({
@@ -9311,6 +9354,7 @@ const TaktPermissionsOverrideSchema = z.looseObject({
9311
9354
  })),
9312
9355
  workflow_runtime_prepare: z.optional(z.object({ custom_scripts: z.optional(z.boolean()) })),
9313
9356
  workflow_command_gates: z.optional(z.object({ custom_scripts: z.optional(z.boolean()) })),
9357
+ workflow_system_commands: z.optional(z.object({ custom_scripts: z.optional(z.boolean()) })),
9314
9358
  sync_conflict_resolver: z.optional(z.object({ auto_approve_tools: z.optional(z.boolean()) })),
9315
9359
  allow_git_hooks: z.optional(z.boolean()),
9316
9360
  allow_git_filters: z.optional(z.boolean())
@@ -9578,6 +9622,7 @@ const CodexSandboxModeSchema = z.enum([
9578
9622
  "workspace-write",
9579
9623
  "danger-full-access"
9580
9624
  ]);
9625
+ const CODEX_U32_MAX = 4294967295;
9581
9626
  /**
9582
9627
  * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
9583
9628
  * value Codex still accepts for backward compatibility, so it is included so
@@ -9646,6 +9691,9 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
9646
9691
  * | `guardian_subagent`), or a table for the richer reviewer config.
9647
9692
  * Defaults to `auto_review` when neither the override nor the existing
9648
9693
  * config sets it.
9694
+ * - `auto_review` — the `[auto_review]` table that configures the reviewer
9695
+ * `approvals_reviewer = "auto_review"` selects (`policy`, `extra_policy`,
9696
+ * `timeout_ms`, …). A loose table, shallow-merged like `apps` / `tui`.
9649
9697
  * - `git_write_rules` — whether the managed profile's `:workspace_roots` table
9650
9698
  * emits the default `.git` carve-out (`".git/**" = "write"`). Codex's
9651
9699
  * `:workspace` baseline makes `.git` read-only, which denies basic git
@@ -9659,7 +9707,8 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
9659
9707
  * The keys written to `config.toml` are an **allowlist**, not verbatim
9660
9708
  * passthrough: only `CODEXCLI_OVERRIDE_KEYS`
9661
9709
  * (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
9662
- * `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
9710
+ * `sandbox_workspace_write`, `apps`, `approvals_reviewer`, `auto_review`,
9711
+ * `tui`) are emitted, and
9663
9712
  * `computeCodexcliOverridePatch` skips anything else with a warning.
9664
9713
  * `base_permission_profile` and `git_write_rules` are consumed by the profile
9665
9714
  * builder rather than written, as described above, and `permission` is the
@@ -9691,6 +9740,26 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
9691
9740
  apps: z.optional(z.looseObject({})),
9692
9741
  approvals_reviewer: z.optional(z.union([CodexApprovalsReviewerSchema, z.looseObject({})])),
9693
9742
  /**
9743
+ * The `[auto_review]` table of `config.toml`, which configures the reviewer
9744
+ * that `approvals_reviewer = "auto_review"` (Rulesync's default) selects:
9745
+ * `policy` / `extra_policy` (text inserted into the reviewer prompt),
9746
+ * `timeout_ms` (the review budget, Codex 0.165.0+; `0` waits without a
9747
+ * deadline), `circuit_break_action` (`default` | `strict`) and
9748
+ * `conversation_history_max_output_tokens` (a positive token cap). Codex
9749
+ * rejects the whole config file when one of these has the wrong type or is
9750
+ * out of range (`timeout_ms` is a `u32`, the token cap a `NonZeroUsize`), so
9751
+ * they are checked here; every other key passes through verbatim.
9752
+ *
9753
+ * @see https://github.com/openai/codex/blob/rust-v0.166.0/codex-rs/config/src/config_toml.rs (`AutoReviewToml`)
9754
+ */
9755
+ auto_review: z.optional(z.looseObject({
9756
+ policy: z.optional(z.string()),
9757
+ extra_policy: z.optional(z.string()),
9758
+ timeout_ms: z.optional(z.int().check(z.nonnegative(), z.lte(CODEX_U32_MAX))),
9759
+ circuit_break_action: z.optional(z.enum(["default", "strict"])),
9760
+ conversation_history_max_output_tokens: z.optional(z.int().check(z.positive()))
9761
+ })),
9762
+ /**
9694
9763
  * The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
9695
9764
  * Not a permission surface, but like `apps` it is a top-level table with no
9696
9765
  * canonical category, and Codex adds keys to it faster than an explicit model
@@ -12297,6 +12366,7 @@ const CODEXCLI_OVERRIDE_KEYS = [
12297
12366
  "sandbox_workspace_write",
12298
12367
  "apps",
12299
12368
  "approvals_reviewer",
12369
+ "auto_review",
12300
12370
  "tui"
12301
12371
  ];
12302
12372
  // src/constants/takt-paths.ts
@@ -13719,6 +13789,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13719
13789
  "workflow_arpeggio",
13720
13790
  "workflow_runtime_prepare",
13721
13791
  "workflow_command_gates",
13792
+ "workflow_system_commands",
13722
13793
  "sync_conflict_resolver",
13723
13794
  "allow_git_hooks",
13724
13795
  "allow_git_filters"
@@ -13860,6 +13931,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13860
13931
  "chat.tools.edits.autoApprove",
13861
13932
  "chat.tools.urls.autoApprove",
13862
13933
  "chat.permissions.default",
13934
+ "chat.defaultConfiguration",
13863
13935
  "chat.agent.sandbox.enabled",
13864
13936
  "chat.agent.sandbox.network.allowNetwork",
13865
13937
  "chat.agent.sandbox.network.allowLocalNetwork",
@@ -14118,6 +14190,14 @@ const SHARED_CONFIG_OWNERSHIP = {
14118
14190
  }
14119
14191
  }
14120
14192
  },
14193
+ ".trae/hooks.json": {
14194
+ format: "json",
14195
+ invalidRootPolicy: "error",
14196
+ features: { hooks: {
14197
+ kind: "replace-owned-keys",
14198
+ ownedKeys: ["hooks"]
14199
+ } }
14200
+ },
14121
14201
  ".trae/permission/global.json": {
14122
14202
  format: "json",
14123
14203
  invalidRootPolicy: "error",
@@ -23243,10 +23323,13 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23243
23323
  * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23244
23324
  * @see https://docs.trae.ai/ide/ignore-files?_lang=en
23245
23325
  * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
23326
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
23246
23327
  */
23247
23328
  const TRAE_DIR = ".trae";
23248
23329
  /** Project MCP servers (`mcpServers` object), project scope only. */
23249
23330
  const TRAE_MCP_FILE_NAME = "mcp.json";
23331
+ /** Hooks: `.trae/hooks.json` (project) and `~/.trae/hooks.json` (global). */
23332
+ const TRAE_HOOKS_FILE_NAME = "hooks.json";
23250
23333
  /** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
23251
23334
  const TRAE_SKILLS_DIR_PATH = join(TRAE_DIR, "skills");
23252
23335
  /**
@@ -25171,12 +25254,80 @@ var AiassistantSkill = class AiassistantSkill extends ToolSkill {
25171
25254
  });
25172
25255
  }
25173
25256
  };
25257
+ // src/features/shared/amp-settings.ts
25258
+ /**
25259
+ * Parse an Amp `settings.json` / `settings.jsonc` document.
25260
+ *
25261
+ * Shared by the MCP and permissions adapters because both read the very same
25262
+ * file. Two separately maintained copies drifted once already: the permissions
25263
+ * side was hardened while the MCP side kept calling `jsonc-parser` directly,
25264
+ * which left it importing MCP servers reachable only through an injected
25265
+ * `__proto__`.
25266
+ *
25267
+ * {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
25268
+ * error instead of returning `jsonc-parser`'s best-effort value, and it
25269
+ * rebuilds every object from its own enumerable entries, so a root `__proto__`
25270
+ * cannot swap the returned object's prototype and `constructor` / `prototype`
25271
+ * never survive as own keys, at any depth.
25272
+ *
25273
+ * Removal is silent, matching every other tool-side adapter (opencode, kilo,
25274
+ * copilot). Reporting the removed keys through `droppedPollutionKeysError` is
25275
+ * reserved for the three files a user authors under `.rulesync/`, whose whole
25276
+ * purpose is to be turned into tool config — a key that vanishes there needs
25277
+ * explaining, whereas here the surrounding settings are the user's own file
25278
+ * and are left alone.
25279
+ *
25280
+ * Note the consequence for a root `__proto__`: sanitizing runs before the
25281
+ * plain-object check, so such a document parses successfully with the key
25282
+ * stripped rather than failing that check. That is deliberate — dropping the
25283
+ * one poisoned key keeps the user's unrelated settings — and matches the
25284
+ * sanitize-before-the-root-check design the shared config gateway documents.
25285
+ */
25286
+ function parseAmpSettings({ fileContent }) {
25287
+ let parsed;
25288
+ try {
25289
+ parsed = parseJsonc(fileContent || "{}");
25290
+ } catch (error) {
25291
+ throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
25292
+ }
25293
+ if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
25294
+ return parsed;
25295
+ }
25296
+ /**
25297
+ * Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
25298
+ * an existing user file is read-modified-written in place instead of a fresh
25299
+ * `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
25300
+ * `settings.json` when neither file exists: Amp's canonically documented
25301
+ * settings file (`.amp/settings.json`), shared by the MCP and permissions
25302
+ * adapters, so generating both on a clean repo writes a single settings file.
25303
+ */
25304
+ async function resolveAmpSettingsFile(jsonDir) {
25305
+ const jsoncContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
25306
+ if (jsoncContent !== null) return {
25307
+ fileContent: jsoncContent,
25308
+ relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
25309
+ };
25310
+ const jsonContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_FILE_NAME));
25311
+ if (jsonContent !== null) return {
25312
+ fileContent: jsonContent,
25313
+ relativeFilePath: AMP_SETTINGS_FILE_NAME
25314
+ };
25315
+ return {
25316
+ fileContent: null,
25317
+ relativeFilePath: AMP_SETTINGS_FILE_NAME
25318
+ };
25319
+ }
25174
25320
  // src/features/skills/amp-skill.ts
25175
25321
  const AmpSkillFrontmatterSchema = z.looseObject({
25176
25322
  name: z.string(),
25177
25323
  description: z.string()
25178
25324
  });
25179
25325
  /**
25326
+ * Amp setting that makes it skip `~/.config/agents/skills/` and
25327
+ * `~/.agents/skills/`. https://ampcode.com/docs/cli/settings
25328
+ */
25329
+ const AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY = "amp.skills.disableGlobalAgentsSkills";
25330
+ /**
25180
25331
  * Represents an Amp Agent Skill directory.
25181
25332
  *
25182
25333
  * Amp natively reads Agent Skills following the open standard: a directory with a
@@ -25211,6 +25362,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
25211
25362
  ...global && { importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH, AMP_SKILLS_NATIVE_GLOBAL_DIR] }
25212
25363
  };
25213
25364
  }
25365
+ /**
25366
+ * Warn when the user's Amp settings disable the global agent-agnostic skill
25367
+ * roots, because the global skills rulesync writes to
25368
+ * `~/.config/agents/skills/` would then never load. Only the user settings
25369
+ * file is checked: a workspace `.amp/settings.json(c)` depends on where Amp
25370
+ * is started, which global generation cannot know.
25371
+ */
25372
+ static async getGenerationWarnings({ outputRoot, global, logger }) {
25373
+ if (!global) return [];
25374
+ const { fileContent, relativeFilePath } = await resolveAmpSettingsFile(join(outputRoot, AMP_GLOBAL_DIR));
25375
+ if (fileContent === null) return [];
25376
+ let settings;
25377
+ try {
25378
+ settings = parseAmpSettings({ fileContent });
25379
+ } catch (error) {
25380
+ logger.debug(`Skipping the Amp skills settings check for ${join(AMP_GLOBAL_DIR, relativeFilePath)}: ${formatError(error)}`);
25381
+ return [];
25382
+ }
25383
+ if (settings[AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY] !== true) return [];
25384
+ return [`Amp will not load the global skills rulesync writes to ~/${AMP_SKILLS_GLOBAL_DIR}/: '${AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY}' is true in ~/${join(AMP_GLOBAL_DIR, relativeFilePath)}.`];
25385
+ }
25214
25386
  getFrontmatter() {
25215
25387
  return AmpSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
25216
25388
  }
@@ -33187,7 +33359,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
33187
33359
  async convertRulesyncDirsToToolDirs(rulesyncDirs) {
33188
33360
  const rulesyncSkills = rulesyncDirs.filter((dir) => dir instanceof RulesyncSkill);
33189
33361
  const factory = this.getFactory(this.toolTarget);
33190
- return (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
33362
+ const toolSkills = (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
33191
33363
  const isClaudecodeScheduledTask = rulesyncSkill.getFrontmatter().claudecode?.["scheduled-task"] === true;
33192
33364
  if (isClaudecodeScheduledTask && this.toolTarget !== "claudecode" && this.toolTarget !== "claudecode-legacy") return null;
33193
33365
  if (!factory.class.isTargetedByRulesyncSkill(rulesyncSkill)) return null;
@@ -33214,6 +33386,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
33214
33386
  logger: this.logger
33215
33387
  });
33216
33388
  }))).filter((skill) => skill !== null);
33389
+ if (toolSkills.length > 0 && factory.class.getGenerationWarnings) {
33390
+ const warnings = await factory.class.getGenerationWarnings({
33391
+ outputRoot: this.outputRoot,
33392
+ global: this.global,
33393
+ logger: this.logger
33394
+ });
33395
+ for (const warning of warnings) this.logger.warn(warning);
33396
+ }
33397
+ return toolSkills;
33217
33398
  }
33218
33399
  async convertToolDirsToRulesyncDirs(toolDirs) {
33219
33400
  const toolSkills = toolDirs.filter((dir) => dir instanceof ToolSkill);
@@ -34101,7 +34282,7 @@ function normalizeAugmentcodeToolList(value) {
34101
34282
  return tools.length > 0 ? tools : void 0;
34102
34283
  }
34103
34284
  const AugmentcodeSubagentFrontmatterSchema = z.looseObject({
34104
- name: z.string(),
34285
+ name: z.optional(z.string()),
34105
34286
  description: z.optional(z.string()),
34106
34287
  color: z.optional(z.string()),
34107
34288
  model: z.optional(z.string()),
@@ -34160,7 +34341,7 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
34160
34341
  outputRoot: ".",
34161
34342
  frontmatter: {
34162
34343
  targets: ["*"],
34163
- name,
34344
+ name: name ?? basename(this.getRelativeFilePath(), extname(this.getRelativeFilePath())),
34164
34345
  description,
34165
34346
  ...Object.keys(augmentcodeSection).length > 0 && { augmentcode: augmentcodeSection }
34166
34347
  },
@@ -34226,7 +34407,8 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
34226
34407
  const dirPath = relativeDirPath ?? this.getSettablePaths({ global }).relativeDirPath;
34227
34408
  const filePath = join(outputRoot, dirPath, relativeFilePath);
34228
34409
  const fileContent = await readFileContent(filePath);
34229
- const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
34410
+ const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
34411
+ if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}`);
34230
34412
  const result = AugmentcodeSubagentFrontmatterSchema.safeParse(frontmatter);
34231
34413
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
34232
34414
  return new AugmentcodeSubagent({
@@ -41809,7 +41991,23 @@ function sanitizeOpencodeMcpNamePart(value) {
41809
41991
  * key that matches nothing.
41810
41992
  */
41811
41993
  function opencodeMcpToolPermissionKey(serverName, toolName) {
41812
- return `${sanitizeOpencodeMcpNamePart(serverName)}_${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
41994
+ return `${opencodeMcpServerKeyPrefix(serverName)}${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
41995
+ }
41996
+ /** The sanitized `<server>_` prefix every permission key of the server's tools starts with. */
41997
+ function opencodeMcpServerKeyPrefix(serverName) {
41998
+ return `${sanitizeOpencodeMcpNamePart(serverName)}_`;
41999
+ }
42000
+ /**
42001
+ * Whether `key` is exactly the sanitized `<server>_` prefix of one of
42002
+ * `serverNames` (e.g. `a_b_` for server `a.b` or `a_b`). No tool has an empty
42003
+ * name, so such a key filters nothing of that server, and
42004
+ * {@link findOpencodeMcpToolKeyOwner} gives it no owner. Read as an unlisted
42005
+ * server's filter instead, it would be split at the first `_` into a phantom
42006
+ * server (`a` with tool `b_`), so importers leave it out of the rulesync
42007
+ * config, and the OpenCode family's generate keeps it in the file as it was.
42008
+ */
42009
+ function isBareOpencodeMcpServerPrefix({ key, serverNames }) {
42010
+ return serverNames.some((serverName) => key === opencodeMcpServerKeyPrefix(serverName));
41813
42011
  }
41814
42012
  /**
41815
42013
  * The server among `serverNames` whose tool a permission key names, with the
@@ -41823,7 +42021,7 @@ function findOpencodeMcpToolKeyOwner({ key, serverNames, builtinKeys, acceptRawP
41823
42021
  if (builtinKeys.has(key)) return;
41824
42022
  let owner;
41825
42023
  for (const serverName of serverNames) {
41826
- const sanitized = `${sanitizeOpencodeMcpNamePart(serverName)}_`;
42024
+ const sanitized = opencodeMcpServerKeyPrefix(serverName);
41827
42025
  const prefixes = acceptRawPrefix ? uniq([sanitized, `${serverName}_`]) : [sanitized];
41828
42026
  for (const prefix of prefixes) if (key.length > prefix.length && key.startsWith(prefix) && (owner === void 0 || prefix.length > owner.prefix.length)) owner = {
41829
42027
  serverName,
@@ -42284,7 +42482,10 @@ function convertFromKiloFormat(kiloMcp, tools, permission) {
42284
42482
  const serverNames = Object.keys(kiloMcp);
42285
42483
  return {
42286
42484
  ...Object.fromEntries(Object.entries(kiloMcp).map(([serverName, serverConfig]) => [serverName, kiloServerToRulesync(serverName, serverConfig, splitKiloServerTools(serverName, serverNames, filters))])),
42287
- ...orphanMcpToolFiltersToRulesync(kiloMcp, tools)
42485
+ ...orphanMcpToolFiltersToRulesync(kiloMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findKiloMcpToolKeyOwner(key, serverNames) && !isBareOpencodeMcpServerPrefix({
42486
+ key,
42487
+ serverNames
42488
+ }))))
42288
42489
  };
42289
42490
  }
42290
42491
  /**
@@ -42802,16 +43003,20 @@ function convertFromOpencodeFormat(opencodeMcp, tools, remoteType) {
42802
43003
  ...orphanMcpToolFiltersToRulesync(opencodeMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findOpencodeToolOwner({
42803
43004
  key,
42804
43005
  serverNames
43006
+ }) && !isBareOpencodeMcpServerPrefix({
43007
+ key,
43008
+ serverNames
42805
43009
  })))),
42806
43010
  ...convertOpencodeServers(opencodeMcp, tools, remoteType)
42807
43011
  };
42808
43012
  }
43013
+ const OPENCODE_MCP_BUILTIN_TOOL_KEYS = /* @__PURE__ */ new Set();
42809
43014
  /** Recognize both OpenCode's sanitized names and keys older rulesync versions wrote. */
42810
43015
  function findOpencodeToolOwner({ key, serverNames }) {
42811
43016
  return findOpencodeMcpToolKeyOwner({
42812
43017
  key,
42813
43018
  serverNames,
42814
- builtinKeys: /* @__PURE__ */ new Set(),
43019
+ builtinKeys: OPENCODE_MCP_BUILTIN_TOOL_KEYS,
42815
43020
  acceptRawPrefix: true
42816
43021
  });
42817
43022
  }
@@ -42981,6 +43186,40 @@ function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
42981
43186
  tools
42982
43187
  };
42983
43188
  }
43189
+ /**
43190
+ * Copy each key of `existingTools` that is a bare sanitized `<server>_` prefix
43191
+ * of one of `serverNames` into `tools`, unless a generated filter already
43192
+ * writes the same key or another listed server owns it (`a_b_` is tool `b_` of
43193
+ * server `a` when `a` is listed too). An owned key is the rulesync config's to
43194
+ * write, so carrying it over would keep a filter the user removed. Mutates
43195
+ * `tools` in place.
43196
+ */
43197
+ function preserveBareOpencodeMcpServerPrefixKeys({ tools, existingTools, serverNames }) {
43198
+ for (const [key, enabled] of Object.entries(existingTools ?? {})) if (!Object.hasOwn(tools, key) && isBareOpencodeMcpServerPrefix({
43199
+ key,
43200
+ serverNames
43201
+ }) && !findOpencodeToolOwner({
43202
+ key,
43203
+ serverNames
43204
+ })) tools[key] = enabled;
43205
+ }
43206
+ /**
43207
+ * When the existing file does not pass the schema, its `tools` map cannot be
43208
+ * read, so a bare `<server>_` key in it is not carried over. Say so rather
43209
+ * than lift the deny it may hold without a word.
43210
+ */
43211
+ function warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({ existingJson, serverNames, logger }) {
43212
+ const rawTools = isRecord(existingJson) ? existingJson.tools : void 0;
43213
+ if (!isRecord(rawTools)) return;
43214
+ const bareKeys = Object.keys(rawTools).filter((key) => isBareOpencodeMcpServerPrefix({
43215
+ key,
43216
+ serverNames
43217
+ }) && !findOpencodeToolOwner({
43218
+ key,
43219
+ serverNames
43220
+ }));
43221
+ if (bareKeys.length > 0) logger?.warn(`OpenCode MCP: the existing config does not match the expected schema, so the bare server-prefix tool ${bareKeys.length === 1 ? "key" : "keys"} ${bareKeys.map(quoteValueForWarning).join(", ")} ${bareKeys.length === 1 ? "is" : "are"} not kept in "tools".`);
43222
+ }
42984
43223
  const OpencodeMcpFeatureOptionsSchema = z.looseObject({ configShape: z.optional(z.enum(["v1", "v2"])) });
42985
43224
  var OpencodeMcp = class extends ToolMcp {
42986
43225
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
@@ -43113,6 +43352,16 @@ var OpencodeMcp = class extends ToolMcp {
43113
43352
  convertedMcp,
43114
43353
  mcpServers: transformedServers
43115
43354
  });
43355
+ if (existingMcp.success) preserveBareOpencodeMcpServerPrefixKeys({
43356
+ tools: mcpTools,
43357
+ existingTools: existingMcp.data.tools,
43358
+ serverNames: Object.keys(transformedServers)
43359
+ });
43360
+ else warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({
43361
+ existingJson: this.lowerConfig(existingJson),
43362
+ serverNames: Object.keys(transformedServers),
43363
+ logger
43364
+ });
43116
43365
  if (this.layout.configVersion === "v2") warnAboutOpencodeV2SseServers({
43117
43366
  mcpServers: transformedServers,
43118
43367
  convertedMcp,
@@ -47351,6 +47600,45 @@ var DshRule = class DshRule extends NestedAgentsmdRule {
47351
47600
  };
47352
47601
  }
47353
47602
  };
47603
+ // src/features/rules/output-style-paths.ts
47604
+ /**
47605
+ * Shared helpers for tool rule adapters that write opted-in rules as custom
47606
+ * output styles (`<tool dir>/output-styles/<name>.md`), e.g. Qoder and
47607
+ * Factory Droid. Both tools read only the direct `.md` children of their
47608
+ * output-styles directory, with optional `name` / `description` frontmatter.
47609
+ */
47610
+ /**
47611
+ * Whether the given path is a direct `.md` child of `outputStylesDirPath`.
47612
+ */
47613
+ function isDirectMarkdownChild({ outputStylesDirPath, relativeDirPath, relativeFilePath }) {
47614
+ return relativeDirPath === outputStylesDirPath && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
47615
+ }
47616
+ /**
47617
+ * Whether a rule opts into the `output-style` channel: only a non-root rule
47618
+ * can, since an output style is a file of its own.
47619
+ */
47620
+ function isOutputStyleChannel({ root, channel }) {
47621
+ return !root && channel === "output-style";
47622
+ }
47623
+ /**
47624
+ * Return the file stem of an output-style source rule, throwing `message`
47625
+ * when the rule is nested in a subdirectory of the rules directory: the
47626
+ * output file is named after the rule, and the tools read only the top level
47627
+ * of their output-styles directory.
47628
+ */
47629
+ function resolveTopLevelOutputStyleStem({ sourcePath, message }) {
47630
+ if (basename(sourcePath) !== sourcePath) throw new Error(message);
47631
+ return basename(sourcePath, extname(sourcePath));
47632
+ }
47633
+ /**
47634
+ * The output style's frontmatter: only the `name` and `description` that are set.
47635
+ */
47636
+ function buildOutputStyleFrontmatter({ name, description }) {
47637
+ return {
47638
+ ...name !== void 0 && { name },
47639
+ ...description !== void 0 && { description }
47640
+ };
47641
+ }
47354
47642
  // src/features/rules/factorydroid-rule.ts
47355
47643
  /**
47356
47644
  * The file-name glob `getExtraFixedFiles` returns for the output-styles
@@ -47455,7 +47743,11 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47455
47743
  * child of the output-styles directory, at either scope.
47456
47744
  */
47457
47745
  static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
47458
- return relativeDirPath === this.getOutputStylesDirPath() && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
47746
+ return isDirectMarkdownChild({
47747
+ outputStylesDirPath: this.getOutputStylesDirPath(),
47748
+ relativeDirPath,
47749
+ relativeFilePath
47750
+ });
47459
47751
  }
47460
47752
  /**
47461
47753
  * Which fixed-file channel, if any, owns the given output path. Matching on
@@ -47495,7 +47787,10 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47495
47787
  /** Whether a rule opts into the `output-style` channel (non-root only). */
47496
47788
  static isOutputStyleRule(rulesyncRule) {
47497
47789
  const frontmatter = rulesyncRule.getFrontmatter();
47498
- return !frontmatter.root && frontmatter.factorydroid?.channel === "output-style";
47790
+ return isOutputStyleChannel({
47791
+ root: frontmatter.root,
47792
+ channel: frontmatter.factorydroid?.channel
47793
+ });
47499
47794
  }
47500
47795
  /**
47501
47796
  * Factory Droid loads the channel files itself, so listing one in the root
@@ -47627,15 +47922,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
47627
47922
  static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
47628
47923
  const frontmatter = rulesyncRule.getFrontmatter();
47629
47924
  const sourcePath = rulesyncRule.getRelativeFilePath();
47630
- if (basename(sourcePath) !== sourcePath) throw new Error(`Rule '${sourcePath}' sets factorydroid.channel: output-style but is nested in a subdirectory: Factory Droid loads only direct .md children of .factory/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
47631
- const stem = basename(sourcePath, extname(sourcePath));
47925
+ const stem = resolveTopLevelOutputStyleStem({
47926
+ sourcePath,
47927
+ message: `Rule '${sourcePath}' sets factorydroid.channel: output-style but is nested in a subdirectory: Factory Droid loads only direct .md children of .factory/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
47928
+ });
47632
47929
  const name = frontmatter.factorydroid?.name;
47633
47930
  const styleName = name ?? stem;
47634
47931
  if (FACTORYDROID_RESERVED_OUTPUT_STYLE_NAMES.includes(styleName.trim().toLowerCase())) throw new Error(`Rule '${sourcePath}' sets factorydroid.channel: output-style with the style name '${styleName}', which is reserved: Factory Droid's built-in Default and Concise styles cannot be replaced. Rename the rule file or set factorydroid.name.`);
47635
- const styleFrontmatter = {
47636
- ...name !== void 0 && { name },
47637
- ...frontmatter.description !== void 0 && { description: frontmatter.description }
47638
- };
47932
+ const styleFrontmatter = buildOutputStyleFrontmatter({
47933
+ name,
47934
+ description: frontmatter.description
47935
+ });
47639
47936
  const body = rulesyncRule.getBody();
47640
47937
  return new FactorydroidRule({
47641
47938
  outputRoot,
@@ -49271,12 +49568,19 @@ var QoderRule = class QoderRule extends ToolRule {
49271
49568
  * `.qoder/output-styles/`, at either scope.
49272
49569
  */
49273
49570
  static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
49274
- return relativeDirPath === QODER_OUTPUT_STYLES_DIR_PATH && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
49571
+ return isDirectMarkdownChild({
49572
+ outputStylesDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
49573
+ relativeDirPath,
49574
+ relativeFilePath
49575
+ });
49275
49576
  }
49276
49577
  /** Whether a rule opts into the `output-style` channel (non-root only). */
49277
49578
  static isOutputStyleRule(rulesyncRule) {
49278
49579
  const frontmatter = rulesyncRule.getFrontmatter();
49279
- return !frontmatter.root && frontmatter.qoder?.channel === "output-style";
49580
+ return isOutputStyleChannel({
49581
+ root: frontmatter.root,
49582
+ channel: frontmatter.qoder?.channel
49583
+ });
49280
49584
  }
49281
49585
  /**
49282
49586
  * The output-styles directory, enumerated for import only: it also holds
@@ -49387,16 +49691,18 @@ var QoderRule = class QoderRule extends ToolRule {
49387
49691
  static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
49388
49692
  const { description, qoder } = rulesyncRule.getFrontmatter();
49389
49693
  const sourcePath = rulesyncRule.getRelativeFilePath();
49390
- if (basename(sourcePath) !== sourcePath) throw new Error(`Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
49391
- const name = qoder?.name;
49694
+ const stem = resolveTopLevelOutputStyleStem({
49695
+ sourcePath,
49696
+ message: `Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
49697
+ });
49392
49698
  return new QoderRule({
49393
49699
  outputRoot,
49394
49700
  relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
49395
- relativeFilePath: `${basename(sourcePath, extname(sourcePath))}.md`,
49396
- frontmatter: {
49397
- ...name !== void 0 && { name },
49398
- ...description !== void 0 && { description }
49399
- },
49701
+ relativeFilePath: `${stem}.md`,
49702
+ frontmatter: buildOutputStyleFrontmatter({
49703
+ name: qoder?.name,
49704
+ description
49705
+ }),
49400
49706
  body: rulesyncRule.getBody(),
49401
49707
  validate,
49402
49708
  root: false,
@@ -51667,6 +51973,7 @@ var RulesProcessor = class extends FeatureProcessor {
51667
51973
  factory
51668
51974
  });
51669
51975
  await this.warnForWarpLegacyRootFile(toolRules);
51976
+ this.warnForAugmentcodePluginRules(toolRules);
51670
51977
  await this.warnForInstructionBudget({
51671
51978
  toolRules,
51672
51979
  meta
@@ -52058,6 +52365,20 @@ var RulesProcessor = class extends FeatureProcessor {
52058
52365
  this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
52059
52366
  }
52060
52367
  /**
52368
+ * Auggie 1.x (still a prerelease on npm at the time of writing; the stable
52369
+ * line is 0.36.0) no longer loads `rules/` from plugins: "Rules shipped in
52370
+ * plugins (`rules/` in a plugin)" is listed among the removed features, while
52371
+ * commands, agents, skills, hooks and MCP servers still load. The rule files
52372
+ * are still written, because the stable 0.x CLI reads them, but under 1.x the
52373
+ * bundle silently loses them, so say so.
52374
+ * @see https://docs.augmentcode.com/cli/whats-new
52375
+ * @see https://docs.augmentcode.com/cli/v0/plugins
52376
+ */
52377
+ warnForAugmentcodePluginRules(toolRules) {
52378
+ if (this.toolTarget !== "augmentcode-plugin" || toolRules.length === 0) return;
52379
+ this.logger.warn(`augmentcode-plugin: plugin rules (rules/*.md) are loaded by Auggie 0.x only; Auggie 1.x no longer reads rules/ from plugins, so these ${toolRules.length} rule file(s) have no effect there. Ship instructions that must reach Auggie 1.x users as a skill, or have them generate the augmentcode target in their project.`);
52380
+ }
52381
+ /**
52061
52382
  * Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
52062
52383
  * `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so an
52063
52384
  * `AGENTS.md` this run writes next to one would never be read. That holds for
@@ -56059,9 +56380,9 @@ function groupDefinitionsByMatcher({ definitions, eventName, converterConfig, wa
56059
56380
  });
56060
56381
  translatedMatchers.set(normalizedMatcher, matcher);
56061
56382
  }
56062
- const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
56383
+ const key = [matcher, ...subdividingFields.map(({ canonical, valueType, events }) => {
56063
56384
  const value = def[canonical];
56064
- return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
56385
+ return isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
56065
56386
  })].join("\0");
56066
56387
  const group = byMatcher.get(key);
56067
56388
  if (group) group.defs.push(def);
@@ -56102,6 +56423,7 @@ function runsInPosixShell({ shell, converterConfig }) {
56102
56423
  * Apply the optional project directory variable prefix to a command string.
56103
56424
  */
56104
56425
  function applyCommandPrefix({ def, converterConfig }) {
56426
+ if (converterConfig.stripsProjectDirVarOnImportOnly) return def.command;
56105
56427
  const commandText = def.command;
56106
56428
  const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
56107
56429
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
@@ -56310,12 +56632,16 @@ const describeInvalidObject = ({ tool, canonical, value }) => isPlainObject(valu
56310
56632
  value
56311
56633
  })} Importing it would fail validation on the next run.` : `Dropping "${tool}" while importing a hook: it must be an object.`;
56312
56634
  const describeInvalidRecord = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a map of strings whose keys are non-empty and free of "=", and whose keys and values carry no newline, carriage return or NUL characters.`;
56635
+ function isGroupFieldOnEvent(events, eventName) {
56636
+ return events === void 0 || events.has(eventName);
56637
+ }
56313
56638
  /**
56314
56639
  * Check a value against the shape its field documents. A string field also
56315
56640
  * rejects control characters, matching the canonical `safeString` so an
56316
56641
  * imported value cannot fail validation on the next generate.
56317
56642
  */
56318
56643
  function isGroupPassthroughValue(value, valueType = "object") {
56644
+ if (valueType === "number") return typeof value === "number" && Number.isFinite(value);
56319
56645
  if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
56320
56646
  return isPlainObject(value);
56321
56647
  }
@@ -56325,10 +56651,14 @@ function isGroupPassthroughValue(value, valueType = "object") {
56325
56651
  */
56326
56652
  function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
56327
56653
  const emitted = {};
56328
- for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
56654
+ for (const { canonical, tool, valueType, events } of converterConfig.groupPassthroughFields ?? []) {
56329
56655
  const carried = defs.map((def) => def[canonical]);
56330
56656
  const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
56331
56657
  if (first === void 0) continue;
56658
+ if (!isGroupFieldOnEvent(events, eventName)) {
56659
+ logger?.warn(`"${canonical}" on "${eventName}" hooks is ignored — the tool supports "${tool}" only on ${[...events ?? []].map((event) => `"${event}"`).join(", ")} hooks.`);
56660
+ continue;
56661
+ }
56332
56662
  const firstStable = stableJson(first);
56333
56663
  const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
56334
56664
  if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${quoteValueForWarning(first)} — including any that asked for something else, or for nothing.`);
@@ -56340,9 +56670,9 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
56340
56670
  * Import the configured group-level passthrough fields onto a definition,
56341
56671
  * reversing {@link emitGroupPassthroughFields}.
56342
56672
  */
56343
- function importGroupPassthroughFields({ rawEntry, converterConfig }) {
56673
+ function importGroupPassthroughFields({ rawEntry, eventName, converterConfig }) {
56344
56674
  const entry = rawEntry;
56345
- return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
56675
+ return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType, events }) => isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
56346
56676
  }
56347
56677
  /**
56348
56678
  * Emit the payload fields specific to a hook type — `url`/`headers`/
@@ -56668,19 +56998,23 @@ function stripCommandPrefix({ command, args, shell, converterConfig, warn }) {
56668
56998
  const cmd = typeof command === "string" ? command : void 0;
56669
56999
  const { projectDirVar } = converterConfig;
56670
57000
  if (projectDirVar === "" || typeof cmd !== "string") return cmd;
56671
- const { command: stripped, unrestored, retained } = importProjectDirVariable({
56672
- command: cmd,
56673
- projectDirVar,
56674
- firstWordOnly: isExecFormHook({
56675
- args,
56676
- converterConfig
56677
- }) || !runsInPosixShell({
56678
- shell,
56679
- converterConfig
56680
- })
56681
- });
56682
- if (warn && unrestored) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${projectDirVar} path that is not a script the command runs becomes relative to the hook's working directory and is not restored on generate. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") to keep the variable.`);
56683
- if (warn && retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${projectDirVar} in an assignment or option value, or in a later word of a command whose later words are not normalized, so the imported command still refers to it and other tools that do not define it receive it verbatim. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") if other tools should not see it.`);
57001
+ let stripped = cmd;
57002
+ for (const variable of [projectDirVar, ...converterConfig.importedProjectDirVarAliases ?? []]) {
57003
+ const result = importProjectDirVariable({
57004
+ command: stripped,
57005
+ projectDirVar: variable,
57006
+ firstWordOnly: isExecFormHook({
57007
+ args,
57008
+ converterConfig
57009
+ }) || !runsInPosixShell({
57010
+ shell,
57011
+ converterConfig
57012
+ })
57013
+ });
57014
+ stripped = result.command;
57015
+ if (warn && result.unrestored && !converterConfig.stripsProjectDirVarOnImportOnly) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${variable} path that is not a script the command runs becomes relative to the hook's working directory and is not restored on generate. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") to keep the variable.`);
57016
+ if (warn && result.retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${variable} in an assignment or option value, or in a later word of a command whose later words are not normalized, so the imported command still refers to it and other tools that do not define it receive it verbatim. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") if other tools should not see it.`);
57017
+ }
56684
57018
  return stripped;
56685
57019
  }
56686
57020
  /**
@@ -56721,7 +57055,7 @@ function isStringRecord(value) {
56721
57055
  }
56722
57056
  /** Compare object values without letting key order decide the answer. */
56723
57057
  function stableJson(value) {
56724
- if (typeof value === "string") return JSON.stringify(value);
57058
+ if (typeof value !== "object") return JSON.stringify(value);
56725
57059
  return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
56726
57060
  }
56727
57061
  /**
@@ -56961,6 +57295,7 @@ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn })
56961
57295
  }),
56962
57296
  ...importGroupPassthroughFields({
56963
57297
  rawEntry,
57298
+ eventName,
56964
57299
  converterConfig
56965
57300
  }),
56966
57301
  ...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
@@ -57013,15 +57348,26 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
57013
57348
  * is the group-level twin of `matcher`: importing the group without it would
57014
57349
  * widen every hook in it, so the group is skipped instead.
57015
57350
  */
57016
- function describeGroupSkipReason({ rawEntry, converterConfig }) {
57351
+ function describeGroupSkipReason({ rawEntry, eventName, converterConfig }) {
57017
57352
  const entry = rawEntry;
57018
- for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
57353
+ for (const { tool, valueType, subdividesGroup, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
57019
57354
  const value = entry[tool];
57020
- if (subdividesGroup !== true || value === void 0) continue;
57355
+ if (subdividesGroup !== true || onInvalidImport === "drop-field" || value === void 0 || !isGroupFieldOnEvent(events, eventName)) continue;
57021
57356
  if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${quoteValueForWarning(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
57022
57357
  }
57023
57358
  }
57024
57359
  /**
57360
+ * Warn about each `"drop-field"` group value that is present but unusable;
57361
+ * {@link importGroupPassthroughFields} leaves it off the imported hooks.
57362
+ */
57363
+ function warnDroppedGroupFields({ rawEntry, eventName, converterConfig, warn }) {
57364
+ const entry = rawEntry;
57365
+ for (const { tool, valueType, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
57366
+ const value = entry[tool];
57367
+ if (onInvalidImport === "drop-field" && value !== void 0 && isGroupFieldOnEvent(events, eventName) && !isGroupPassthroughValue(value, valueType)) warn?.(`Dropping the unusable "${tool}" (${quoteValueForWarning(value)}) of a "${eventName}" matcher group while importing; its hooks are imported without it.`);
57368
+ }
57369
+ }
57370
+ /**
57025
57371
  * Why this hook cannot be imported, or `undefined` when it can.
57026
57372
  */
57027
57373
  function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
@@ -57048,12 +57394,19 @@ function toolMatcherEntryToCanonical({ rawEntry, eventName, converterConfig, war
57048
57394
  const hookDefs = rawEntry.hooks ?? [];
57049
57395
  const groupSkipReason = describeGroupSkipReason({
57050
57396
  rawEntry,
57397
+ eventName,
57051
57398
  converterConfig
57052
57399
  });
57053
57400
  if (groupSkipReason !== void 0) {
57054
57401
  warn?.(groupSkipReason);
57055
57402
  return [];
57056
57403
  }
57404
+ warnDroppedGroupFields({
57405
+ rawEntry,
57406
+ eventName,
57407
+ converterConfig,
57408
+ warn
57409
+ });
57057
57410
  const definitions = [];
57058
57411
  for (const h of hookDefs) {
57059
57412
  const skipReason = describeHookSkipReason({
@@ -60392,6 +60745,23 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
60392
60745
  });
60393
60746
  }
60394
60747
  };
60748
+ // src/features/hooks/hook-matcher-tool-names.ts
60749
+ const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60750
+ function isMatcherName(token) {
60751
+ return !token.startsWith("\\") && !token.startsWith("[");
60752
+ }
60753
+ /**
60754
+ * Rewrites the whole tool names inside a hook `matcher` regex through `names`,
60755
+ * leaving escapes, character classes and unknown names as written. Used by
60756
+ * adapters whose runtime tool names differ from the canonical (Claude Code)
60757
+ * spelling.
60758
+ */
60759
+ function replaceMatcherNames(matcher, names) {
60760
+ return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60761
+ }
60762
+ function invertToolNames(names) {
60763
+ return Object.fromEntries(Object.entries(names).map(([canonical, tool]) => [tool, canonical]));
60764
+ }
60395
60765
  // src/features/hooks/cortexcode-hooks.ts
60396
60766
  const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
60397
60767
  const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
@@ -60402,14 +60772,7 @@ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
60402
60772
  Grep: "grep",
60403
60773
  Glob: "glob"
60404
60774
  };
60405
- const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_TOOL_NAMES).map(([canonical, tool]) => [tool, canonical]));
60406
- const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
60407
- function isMatcherName(token) {
60408
- return !token.startsWith("\\") && !token.startsWith("[");
60409
- }
60410
- function replaceMatcherNames(matcher, names) {
60411
- return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
60412
- }
60775
+ const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60413
60776
  function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
60414
60777
  const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
60415
60778
  const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
@@ -65570,6 +65933,61 @@ var TabnineHooks = class TabnineHooks extends ToolHooks {
65570
65933
  });
65571
65934
  }
65572
65935
  };
65936
+ // src/features/hooks/trae-hooks.ts
65937
+ const TRAE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
65938
+ "sessionStart",
65939
+ "beforeSubmitPrompt",
65940
+ "stop"
65941
+ ]);
65942
+ const CANONICAL_TO_TRAE_TOOL_NAMES = { Bash: "RunCommand" };
65943
+ const TRAE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_TRAE_TOOL_NAMES);
65944
+ const TRAE_SPEC = {
65945
+ displayName: "TRAE",
65946
+ overrideKey: "trae",
65947
+ converterConfig: {
65948
+ supportedEvents: TRAE_HOOK_EVENTS,
65949
+ canonicalToToolEventNames: CANONICAL_TO_TRAE_EVENT_NAMES,
65950
+ toolToCanonicalEventNames: TRAE_TO_CANONICAL_EVENT_NAMES,
65951
+ projectDirVar: "$TRAE_PROJECT_DIR",
65952
+ stripsProjectDirVarOnImportOnly: true,
65953
+ importedProjectDirVarAliases: ["$CLAUDE_PROJECT_DIR"],
65954
+ noMatcherEvents: TRAE_NO_MATCHER_EVENTS,
65955
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
65956
+ groupPassthroughFields: [{
65957
+ canonical: "loop_limit",
65958
+ tool: "loop_limit",
65959
+ valueType: "number",
65960
+ events: /* @__PURE__ */ new Set(["stop"]),
65961
+ subdividesGroup: true,
65962
+ onInvalidImport: "drop-field"
65963
+ }],
65964
+ matcherTranslation: {
65965
+ toTool: ({ matcher }) => replaceMatcherNames(matcher, CANONICAL_TO_TRAE_TOOL_NAMES),
65966
+ toCanonical: (matcher) => replaceMatcherNames(matcher, TRAE_TO_CANONICAL_TOOL_NAMES)
65967
+ }
65968
+ }
65969
+ };
65970
+ /**
65971
+ * TRAE hooks.
65972
+ *
65973
+ * Hooks live under the top-level `hooks` key of the dedicated `.trae/hooks.json`
65974
+ * (project) and `~/.trae/hooks.json` (user scope). The IDE also writes a
65975
+ * `version` key there, so generation merges the `hooks` key into the file (see
65976
+ * `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
65977
+ *
65978
+ * @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
65979
+ */
65980
+ var TraeHooks = class extends SettingsJsonHooks {
65981
+ static getSpec() {
65982
+ return TRAE_SPEC;
65983
+ }
65984
+ static getSettablePaths(_options = {}) {
65985
+ return {
65986
+ relativeDirPath: TRAE_DIR,
65987
+ relativeFilePath: TRAE_HOOKS_FILE_NAME
65988
+ };
65989
+ }
65990
+ };
65573
65991
  // src/features/hooks/vibe-hooks.ts
65574
65992
  const VIBE_DIR = ".vibe";
65575
65993
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -66754,6 +67172,21 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
66754
67172
  supportedHookTypes: ["command"],
66755
67173
  supportsMatcher: true
66756
67174
  }],
67175
+ ["trae", {
67176
+ class: TraeHooks,
67177
+ meta: {
67178
+ supportsProject: true,
67179
+ supportsGlobal: true,
67180
+ supportsImport: true
67181
+ },
67182
+ ...converterCapabilities(TraeHooks.getConverterConfig()),
67183
+ supportsMatcher: true,
67184
+ matcherEvents: [
67185
+ "preToolUse",
67186
+ "postToolUse",
67187
+ "notification"
67188
+ ]
67189
+ }],
66757
67190
  ["vibe", {
66758
67191
  class: VibeHooks,
66759
67192
  meta: {
@@ -68862,69 +69295,6 @@ var AiassistantMcp = class extends ToolMcp {
68862
69295
  });
68863
69296
  }
68864
69297
  };
68865
- // src/features/shared/amp-settings.ts
68866
- /**
68867
- * Parse an Amp `settings.json` / `settings.jsonc` document.
68868
- *
68869
- * Shared by the MCP and permissions adapters because both read the very same
68870
- * file. Two separately maintained copies drifted once already: the permissions
68871
- * side was hardened while the MCP side kept calling `jsonc-parser` directly,
68872
- * which left it importing MCP servers reachable only through an injected
68873
- * `__proto__`.
68874
- *
68875
- * {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
68876
- * error instead of returning `jsonc-parser`'s best-effort value, and it
68877
- * rebuilds every object from its own enumerable entries, so a root `__proto__`
68878
- * cannot swap the returned object's prototype and `constructor` / `prototype`
68879
- * never survive as own keys, at any depth.
68880
- *
68881
- * Removal is silent, matching every other tool-side adapter (opencode, kilo,
68882
- * copilot). Reporting the removed keys through `droppedPollutionKeysError` is
68883
- * reserved for the three files a user authors under `.rulesync/`, whose whole
68884
- * purpose is to be turned into tool config — a key that vanishes there needs
68885
- * explaining, whereas here the surrounding settings are the user's own file
68886
- * and are left alone.
68887
- *
68888
- * Note the consequence for a root `__proto__`: sanitizing runs before the
68889
- * plain-object check, so such a document parses successfully with the key
68890
- * stripped rather than failing that check. That is deliberate — dropping the
68891
- * one poisoned key keeps the user's unrelated settings — and matches the
68892
- * sanitize-before-the-root-check design the shared config gateway documents.
68893
- */
68894
- function parseAmpSettings({ fileContent }) {
68895
- let parsed;
68896
- try {
68897
- parsed = parseJsonc(fileContent || "{}");
68898
- } catch (error) {
68899
- throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
68900
- }
68901
- if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
68902
- return parsed;
68903
- }
68904
- /**
68905
- * Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
68906
- * an existing user file is read-modified-written in place instead of a fresh
68907
- * `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
68908
- * `settings.json` when neither file exists: Amp's canonically documented
68909
- * settings file (`.amp/settings.json`), shared by the MCP and permissions
68910
- * adapters, so generating both on a clean repo writes a single settings file.
68911
- */
68912
- async function resolveAmpSettingsFile(jsonDir) {
68913
- const jsoncContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
68914
- if (jsoncContent !== null) return {
68915
- fileContent: jsoncContent,
68916
- relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
68917
- };
68918
- const jsonContent = await readFileContentOrNull(join(jsonDir, AMP_SETTINGS_FILE_NAME));
68919
- if (jsonContent !== null) return {
68920
- fileContent: jsonContent,
68921
- relativeFilePath: AMP_SETTINGS_FILE_NAME
68922
- };
68923
- return {
68924
- fileContent: null,
68925
- relativeFilePath: AMP_SETTINGS_FILE_NAME
68926
- };
68927
- }
68928
69298
  // src/features/mcp/amp-mcp.ts
68929
69299
  const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
68930
69300
  function filterMcpServers(mcpServers) {
@@ -71910,7 +72280,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
71910
72280
  fileKey: sharedConfigFileKey(paths),
71911
72281
  feature: "mcp",
71912
72282
  existingContent,
71913
- patch: { servers: convertToCopilotFormat(mcpServers) },
72283
+ patch: { servers: undefinedWhenEmpty(convertToCopilotFormat(mcpServers)) },
71914
72284
  filePath,
71915
72285
  logger
71916
72286
  }),
@@ -74231,14 +74601,20 @@ function resolveGooseUrl(config) {
74231
74601
  }
74232
74602
  /**
74233
74603
  * Determines the Goose `type` for a server based on its command/url/transport.
74604
+ *
74605
+ * Goose no longer has an SSE extension type (it skips a `type: sse` entry as
74606
+ * unsupported), so every remote server is written as `streamable_http`.
74234
74607
  */
74235
74608
  function resolveGooseType(config, url) {
74236
74609
  if (config.command !== void 0) return "stdio";
74237
- if (url !== void 0) return canonicalTransport(config) === "sse" ? "sse" : "streamable_http";
74610
+ if (url !== void 0) return "streamable_http";
74238
74611
  return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
74239
74612
  }
74240
74613
  /**
74241
- * The Goose extension types that carry an MCP server. Goose also documents
74614
+ * The Goose extension types that carry an MCP server. `sse` is no longer one
74615
+ * Goose reads, but it stays here so a stale `type: sse` entry is still
74616
+ * recognized: imported as canonical `sse` and replaced by the regenerated
74617
+ * same-name `streamable_http` entry. Goose also documents
74242
74618
  * `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
74243
74619
  * no canonical MCP counterpart: they name capabilities Goose provides itself
74244
74620
  * rather than a server rulesync could describe.
@@ -74298,7 +74674,8 @@ function convertServerToGooseExtension(name, config, logger) {
74298
74674
  warnWithFallback(logger, `Goose extension "${name}" has no command to run; skipping it rather than writing a stdio extension Goose cannot start to ~/.config/goose/config.yaml.`);
74299
74675
  return;
74300
74676
  }
74301
- } else if (gooseType === "sse" || gooseType === "streamable_http") {
74677
+ } else if (gooseType === "streamable_http") {
74678
+ if (canonicalTransport(config) === "sse") warnWithFallback(logger, `Goose extension "${name}" uses SSE, which Goose no longer supports; writing it as streamable_http, which only works if the server also speaks Streamable HTTP.`);
74302
74679
  if (url !== void 0) ext.uri = url;
74303
74680
  if (isPlainObject(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
74304
74681
  }
@@ -74311,8 +74688,9 @@ function convertServerToGooseExtension(name, config, logger) {
74311
74688
  * Converts rulesync canonical MCP servers into Goose `extensions:` entries.
74312
74689
  *
74313
74690
  * Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
74314
- * | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
74315
- * remote, plus `enabled` and `timeout`.
74691
+ * | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for remote, plus
74692
+ * `enabled` and `timeout`. A canonical `sse` server is written as
74693
+ * `streamable_http` with a warning, since Goose dropped SSE support.
74316
74694
  *
74317
74695
  * `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
74318
74696
  * holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
@@ -74443,7 +74821,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
74443
74821
  * `~/.config/goose/config.yaml`. That file also holds other Goose settings
74444
74822
  * (model, provider, ...), so generation merges the `extensions:` block into the
74445
74823
  * existing config instead of overwriting it, and the file is never deleted.
74446
- * This location supports both stdio and remote (http/sse) servers.
74824
+ * This location supports both stdio and remote servers (SSE is written as
74825
+ * `streamable_http`, since Goose no longer reads `type: sse`).
74447
74826
  * - **Project**: a stdio-only open-plugin manifest at
74448
74827
  * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
74449
74828
  * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
@@ -79161,6 +79540,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
79161
79540
  const managedNames = new Set(Object.keys(rulesyncServers));
79162
79541
  const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
79163
79542
  const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
79543
+ const servers = [...mcpServers, ...unmanagedServers];
79164
79544
  return new VibeMcp({
79165
79545
  outputRoot,
79166
79546
  relativeDirPath: paths.relativeDirPath,
@@ -79169,7 +79549,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
79169
79549
  fileKey: sharedConfigFileKey(paths),
79170
79550
  feature: "mcp",
79171
79551
  existingContent,
79172
- patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
79552
+ patch: { mcp_servers: servers.length > 0 ? servers : void 0 },
79173
79553
  filePath
79174
79554
  }),
79175
79555
  validate,
@@ -82300,6 +82680,57 @@ function matchesGlobStep(step, character) {
82300
82680
  const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
82301
82681
  return step.negated ? !admitted : admitted;
82302
82682
  }
82683
+ /**
82684
+ * Whether `value` matches `glob` in full, with `*`, `?` and `[...]` carrying
82685
+ * their usual meaning. The glob comes first, unlike Node's own
82686
+ * `path.matchesGlob(path, pattern)`.
82687
+ *
82688
+ * The glob is walked step by step rather than translated to a regex, because
82689
+ * both sides of a comparison can come from a file a repository carries: `*a*a*a*a*b`
82690
+ * translated to `^.*a.*a.*a.*a.*b$` takes minutes against a forty-character
82691
+ * name, while walking the steps is linear in the name per `*`. Remembering only
82692
+ * the most recent `*` to fall back to is enough — an earlier one can always give
82693
+ * up whatever the later one needed.
82694
+ */
82695
+ function matchesGlob(glob, value) {
82696
+ return compileGlob(glob)(value);
82697
+ }
82698
+ /**
82699
+ * Parse `glob` once and return a predicate that walks it, for a caller that
82700
+ * tests the same glob against a whole list of names.
82701
+ */
82702
+ function compileGlob(glob) {
82703
+ const steps = parseGlob(glob);
82704
+ return (value) => matchesParsedGlob(steps, value);
82705
+ }
82706
+ function matchesParsedGlob(steps, value) {
82707
+ const characters = [...value];
82708
+ let stepIndex = 0;
82709
+ let characterIndex = 0;
82710
+ let starStepIndex = -1;
82711
+ let starCharacterIndex = 0;
82712
+ while (characterIndex < characters.length) {
82713
+ const step = steps[stepIndex];
82714
+ if (step?.kind === "star") {
82715
+ starStepIndex = stepIndex;
82716
+ starCharacterIndex = characterIndex;
82717
+ stepIndex += 1;
82718
+ continue;
82719
+ }
82720
+ if (step !== void 0 && matchesGlobStep(step, characters[characterIndex] ?? "")) {
82721
+ stepIndex += 1;
82722
+ characterIndex += 1;
82723
+ continue;
82724
+ }
82725
+ if (starStepIndex < 0) return false;
82726
+ starCharacterIndex += 1;
82727
+ stepIndex = starStepIndex + 1;
82728
+ characterIndex = starCharacterIndex;
82729
+ }
82730
+ let remaining = stepIndex;
82731
+ while (steps[remaining]?.kind === "star") remaining += 1;
82732
+ return remaining === steps.length;
82733
+ }
82303
82734
  /** Whether two single-character steps can both match one same character. */
82304
82735
  function stepsShareACharacter(left, right) {
82305
82736
  if (left.kind === "any" || right.kind === "any") return true;
@@ -82542,6 +82973,20 @@ function isUnicodeClass(rest) {
82542
82973
  }));
82543
82974
  }
82544
82975
  /**
82976
+ * Return a function that, for a `[:` at `index`, gives the position of the
82977
+ * next `:]` (Go reads everything between as the class name), or -1. The
82978
+ * position is cached and `index` only grows, so a long run of `[:` with no
82979
+ * `:]` costs linear time rather than a scan to the end at each one.
82980
+ */
82981
+ function posixEndFinder(source) {
82982
+ let next = -2;
82983
+ return (index) => {
82984
+ if (!source.startsWith("[:", index)) return -1;
82985
+ if (next !== -1 && next < index + 2) next = source.indexOf(":]", index + 2);
82986
+ return next;
82987
+ };
82988
+ }
82989
+ /**
82545
82990
  * Replace each POSIX class (`[:alpha:]`) inside a bracket class with a plain
82546
82991
  * character, so JavaScript can compile the rest, or return `undefined` for an
82547
82992
  * unknown one. Only inside a bracket is `[:name:]` a POSIX class; a bare
@@ -82551,6 +82996,7 @@ function replacePosixClasses(source) {
82551
82996
  let result = "";
82552
82997
  let inClass = false;
82553
82998
  let index = 0;
82999
+ const findPosixEnd = posixEndFinder(source);
82554
83000
  while (index < source.length) {
82555
83001
  const character = source[index] ?? "";
82556
83002
  if (character === "\\") {
@@ -82575,11 +83021,11 @@ function replacePosixClasses(source) {
82575
83021
  }
82576
83022
  continue;
82577
83023
  }
82578
- const posix = /^\[:\^?([\s\S]*?):\]/.exec(source.slice(index));
82579
- if (posix !== null) {
82580
- if (!POSIX_CLASSES.has(posix[1] ?? "")) return;
83024
+ const posixEnd = findPosixEnd(index);
83025
+ if (posixEnd !== -1) {
83026
+ if (!POSIX_CLASSES.has(source.slice(index + 2, posixEnd).replace(/^\^/, ""))) return;
82581
83027
  result += "a";
82582
- index += posix[0].length;
83028
+ index = posixEnd + 2;
82583
83029
  continue;
82584
83030
  }
82585
83031
  if (character === "]") inClass = false;
@@ -82658,6 +83104,7 @@ function hasClassEscapeRangeEnd(source) {
82658
83104
  let inClass = false;
82659
83105
  let afterCharacter = false;
82660
83106
  let index = 0;
83107
+ const findPosixEnd = posixEndFinder(source);
82661
83108
  while (index < source.length) {
82662
83109
  const character = source[index] ?? "";
82663
83110
  if (character === "\\") {
@@ -82673,7 +83120,7 @@ function hasClassEscapeRangeEnd(source) {
82673
83120
  });
82674
83121
  continue;
82675
83122
  }
82676
- const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
83123
+ const posixEnd = findPosixEnd(index);
82677
83124
  if (character === "]" || posixEnd !== -1) {
82678
83125
  inClass = character !== "]";
82679
83126
  afterCharacter = false;
@@ -83235,6 +83682,8 @@ function buildToolPatternEntry({ toolName, pattern }) {
83235
83682
  * - `execute_url` takes a domain, like `read_url`.
83236
83683
  * - `command` and `unsandboxed` take literal words or a per-word `regex:`,
83237
83684
  * matched word by word (see `antigravity-command-patterns.ts`).
83685
+ * - `mcp` takes `server/tool` or `server/*`, matched exactly. A canonical
83686
+ * `mcp__<server>__<tool>` category is written as that target.
83238
83687
  *
83239
83688
  * A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
83240
83689
  * matches nothing, so a deny written that way blocks nothing. A pattern with
@@ -83258,7 +83707,8 @@ const DENY_HINTS = {
83258
83707
  read_file: "deny a directory or file path instead",
83259
83708
  write_file: "deny a directory or file path instead",
83260
83709
  command: "deny the first words of the command instead",
83261
- unsandboxed: "deny the first words of the command instead"
83710
+ unsandboxed: "deny the first words of the command instead",
83711
+ mcp: "deny the whole MCP server or tool instead"
83262
83712
  };
83263
83713
  function hasGlob(pattern) {
83264
83714
  return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
@@ -83314,6 +83764,40 @@ function toTarget(action, pattern) {
83314
83764
  if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
83315
83765
  return { target: pattern };
83316
83766
  }
83767
+ const MCP_CATEGORY_PREFIX = "mcp__";
83768
+ const MCP_TARGET_SEPARATORS = /[/()]/;
83769
+ /**
83770
+ * The `mcp` target of a canonical `mcp__<server>__<tool>` category:
83771
+ * `<server>/<tool>`, `<server>/*` for a whole server (`mcp__<server>`,
83772
+ * `mcp__<server>__*`), or `*` for every MCP tool (`mcp__*`, `mcp__*__*`). The
83773
+ * server ends at the first `__`, as elsewhere in rulesync. Antigravity matches
83774
+ * names exactly, so a glob, an empty name, or a `/` or parenthesis in a name
83775
+ * has no target; when only the tool part is at fault, its server is the `lead`
83776
+ * a deny widens to.
83777
+ */
83778
+ function toMcpTarget(category) {
83779
+ const remainder = category.slice(5);
83780
+ const separator = remainder.indexOf("__");
83781
+ const server = separator === -1 ? remainder : remainder.slice(0, separator);
83782
+ const tool = separator === -1 ? "*" : remainder.slice(separator + 2);
83783
+ if (server === "*" && tool === "*") return { target: "*" };
83784
+ const skipReason = "Antigravity MCP targets name one server or one tool exactly. Only `mcp__<server>`, `mcp__<server>__*`, `mcp__<server>__<tool>` or `mcp__*` can be expressed";
83785
+ if (server.length === 0 || MCP_TARGET_SEPARATORS.test(server) || hasGlob(server)) return { skipReason };
83786
+ if (tool === "*") return { target: `${server}/*` };
83787
+ return tool.length === 0 || MCP_TARGET_SEPARATORS.test(tool) || hasGlob(tool) ? {
83788
+ skipReason,
83789
+ lead: `${server}/*`
83790
+ } : { target: `${server}/${tool}` };
83791
+ }
83792
+ /** The target of a canonical `mcp__...` rule, with the `lead` a deny widens to. */
83793
+ function toMcpRuleTarget(category, pattern) {
83794
+ const target = toMcpTarget(category);
83795
+ if (pattern === "*" || !("target" in target)) return target;
83796
+ return {
83797
+ skipReason: "Antigravity MCP targets name a server or a tool, not the tool's arguments",
83798
+ lead: target.target
83799
+ };
83800
+ }
83317
83801
  /**
83318
83802
  * The leading directories of a path glob (`src` for `src/*.ts`), or
83319
83803
  * `undefined` when it has none. A directory target covers everything inside
@@ -83336,6 +83820,20 @@ function toWidenedDenyTarget(action, pattern) {
83336
83820
  if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
83337
83821
  if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
83338
83822
  }
83823
+ /** The entry a deny is widened to, and what it now blocks. */
83824
+ function describeWidenedDeny({ action, prefix, isMcp }) {
83825
+ if (isMcp) return prefix === "*" ? {
83826
+ entry: action,
83827
+ scope: "every MCP tool"
83828
+ } : {
83829
+ entry: `${action}(${prefix})`,
83830
+ scope: `every call of \`${action}(${prefix})\``
83831
+ };
83832
+ return {
83833
+ entry: `${action}(${prefix})`,
83834
+ scope: PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``
83835
+ };
83836
+ }
83339
83837
  /**
83340
83838
  * Warn about a path target that is a home or variable reference. Antigravity
83341
83839
  * documents absolute and workspace-relative paths only, so such a reference
@@ -83357,14 +83855,20 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
83357
83855
  * nothing enforces it, and a widened one is warned about too, because it
83358
83856
  * blocks more than it says.
83359
83857
  */
83360
- function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
83361
- const result = pattern === "*" ? { target: "*" } : toTarget(action, pattern);
83858
+ function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
83859
+ const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
83860
+ const action = isMcp ? "mcp" : toolAction;
83861
+ const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
83362
83862
  const rule = `${category}: { "${pattern}": "${decision}" }`;
83363
83863
  if ("skipReason" in result) {
83364
- const prefix = decision === "deny" ? toWidenedDenyTarget(action, pattern) : void 0;
83864
+ const prefix = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
83365
83865
  if (prefix !== void 0) {
83366
- const scope = PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``;
83367
- logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
83866
+ const { entry, scope } = describeWidenedDeny({
83867
+ action,
83868
+ prefix,
83869
+ isMcp
83870
+ });
83871
+ logger.warn(`${toolLabel} permissions: widening ${rule} to ${entry}. ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
83368
83872
  warnHomeOrVariablePath({
83369
83873
  action,
83370
83874
  target: prefix,
@@ -83373,7 +83877,7 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
83373
83877
  logger,
83374
83878
  toolLabel
83375
83879
  });
83376
- return `${action}(${prefix})`;
83880
+ return entry;
83377
83881
  }
83378
83882
  const consequence = decision === "deny" ? `This deny is NOT enforced in ${toolLabel}; ${DENY_HINTS[action] ?? "write it in a form Antigravity can match"}` : decision === "ask" ? `${toolLabel} falls back to its default for it, so it runs without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} falls back to its default for it`;
83379
83883
  logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
@@ -83517,6 +84021,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
83517
84021
  mcp: "mcp"
83518
84022
  };
83519
84023
  function toAntigravityCliToolName(canonical) {
84024
+ if (canonical.startsWith("mcp__")) return "mcp";
83520
84025
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
83521
84026
  }
83522
84027
  function toCanonicalToolName$9(cliName) {
@@ -83579,10 +84084,11 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
83579
84084
  logger
83580
84085
  });
83581
84086
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
84087
+ const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
83582
84088
  const existingPermissions = settings.permissions ?? {};
83583
- const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83584
- const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83585
- const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
84089
+ const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
84090
+ const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
84091
+ const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
83586
84092
  const mergedPermissions = { ...existingPermissions };
83587
84093
  const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
83588
84094
  const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
@@ -83691,6 +84197,7 @@ const IDE_ACTION_TO_CANONICAL = {
83691
84197
  mcp: "mcp"
83692
84198
  };
83693
84199
  function toIdeAction(canonical) {
84200
+ if (canonical.startsWith("mcp__")) return "mcp";
83694
84201
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
83695
84202
  }
83696
84203
  function toCanonicalCategory$2(ideAction) {
@@ -83747,8 +84254,9 @@ var AntigravityIdePermissions = class AntigravityIdePermissions extends ToolPerm
83747
84254
  logger
83748
84255
  });
83749
84256
  const managedActions = new Set(Object.keys(config.permission).map((category) => toIdeAction(category)));
84257
+ const isManagedAction = (action) => managedActions.has(action) || managedActions.has("mcp") && action.startsWith("mcp__");
83750
84258
  const existingPermissions = settings.permissions ?? {};
83751
- const preserve = (entries) => (entries ?? []).filter((entry) => !managedActions.has(parseToolPatternEntry(entry).toolName));
84259
+ const preserve = (entries) => (entries ?? []).filter((entry) => !isManagedAction(parseToolPatternEntry(entry).toolName));
83752
84260
  const mergedPermissions = { ...existingPermissions };
83753
84261
  const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
83754
84262
  const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
@@ -83836,9 +84344,13 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
83836
84344
  const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
83837
84345
  const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
83838
84346
  terminal: "launch-process",
84347
+ bash: "launch-process",
84348
+ shell: "launch-process",
83839
84349
  read: "view",
83840
84350
  edit: "str-replace-editor",
83841
- write: "save-file"
84351
+ str_replace_editor: "str-replace-editor",
84352
+ write: "save-file",
84353
+ save_file: "save-file"
83842
84354
  };
83843
84355
  function toAugmentToolName(canonical) {
83844
84356
  return lookupOwn({
@@ -83990,6 +84502,7 @@ function isShellRegexRoundtrippable(regex) {
83990
84502
  while (i < body.length) {
83991
84503
  const ch = body[i];
83992
84504
  if (ch === "\\" && i + 1 < body.length) {
84505
+ if (isGlobSpecial(body[i + 1])) return false;
83993
84506
  i += 2;
83994
84507
  continue;
83995
84508
  }
@@ -84006,6 +84519,37 @@ function isShellRegexRoundtrippable(regex) {
84006
84519
  }
84007
84520
  return true;
84008
84521
  }
84522
+ /** Characters a glob reads as syntax (wildcards or the start of a character class). */
84523
+ function isGlobSpecial(ch) {
84524
+ return ch === "*" || ch === "?" || ch === "[";
84525
+ }
84526
+ /**
84527
+ * Whether `shellRegexToGlob` can approximate a `shellInputRegex` without widening it. The lossy
84528
+ * conversion keeps regex quantifiers as characters, and a `*` or `?` left in the glob becomes a
84529
+ * wildcard: `^rm -rf /tmp/a*$` (a run of `a`s) would turn into the glob `rm -rf /tmp/a*`, which
84530
+ * matches `rm -rf /tmp/a /home`. So a regex is unsafe when it has an escaped literal `\*` / `\?` /
84531
+ * `\[` (a `[` would open a character class in targets whose globs support one),
84532
+ * or a bare quantifier `*` / `?` / `+` / `{` that is not part of a `.*` run (which the glob `*`
84533
+ * reproduces faithfully).
84534
+ */
84535
+ function canApproximateShellRegexWithoutWidening(regex) {
84536
+ let i = 0;
84537
+ while (i < regex.length) {
84538
+ const ch = regex[i];
84539
+ if (ch === "\\" && i + 1 < regex.length) {
84540
+ if (isGlobSpecial(regex[i + 1])) return false;
84541
+ i += 2;
84542
+ continue;
84543
+ }
84544
+ if (ch === "." && regex[i + 1] === "*") {
84545
+ i += 2;
84546
+ continue;
84547
+ }
84548
+ if (ch === "*" || ch === "?" || ch === "+" || ch === "{") return false;
84549
+ i += 1;
84550
+ }
84551
+ return true;
84552
+ }
84009
84553
  const MANAGED_AUGMENT_TOOL_NAMES = new Set(Object.values(CANONICAL_TO_AUGMENT_TOOL_NAMES));
84010
84554
  var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermissions {
84011
84555
  constructor(params) {
@@ -84442,12 +84986,18 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
84442
84986
  if (legacyToolName === "launch-process" && entry.shellInputRegex) {
84443
84987
  const regex = entry.shellInputRegex;
84444
84988
  if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
84445
- else if (action === "deny") {
84446
- logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as the catch-all '*' pattern (fail-closed) so the deny rule cannot be silently narrowed on regenerate.`);
84447
- pattern = "*";
84448
- } else {
84449
- pattern = shellRegexToGlob(regex);
84450
- logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as glob '${pattern}'; the rule may match a different set of inputs after regenerate.`);
84989
+ else {
84990
+ if (action !== "deny" && !canApproximateShellRegexWithoutWidening(regex)) {
84991
+ logger?.warn(`AugmentCode permissions: skipping ${action} entry for tool '${entry.toolName}': shellInputRegex '${regex}' uses a quantifier or a literal '*' / '?' that a glob cannot express, and converting it would widen the rule. The entry is not imported.`);
84992
+ continue;
84993
+ }
84994
+ if (action === "deny") {
84995
+ logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as the catch-all '*' pattern (fail-closed) so the deny rule cannot be silently narrowed on regenerate.`);
84996
+ pattern = "*";
84997
+ } else {
84998
+ pattern = shellRegexToGlob(regex);
84999
+ logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as glob '${pattern}'; the rule may match a different set of inputs after regenerate.`);
85000
+ }
84451
85001
  }
84452
85002
  } else pattern = "*";
84453
85003
  if (isPrototypePollutionKey(pattern)) {
@@ -85940,15 +86490,19 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
85940
86490
  * `env` variables a project or local settings file cannot set, because a
85941
86491
  * checked-out repository should not control them: where Claude Code stores its
85942
86492
  * own files, what session content it exports, where telemetry goes, how it
85943
- * starts or syncs, the timers on an unanswered dialog, and whether it processes
85944
- * attachments or expands `@` file mentions, and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
86493
+ * starts or syncs, which claude.ai sign-in it uses, the timers on an unanswered
86494
+ * dialog, and whether it processes attachments or expands `@` file mentions,
86495
+ * and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
85945
86496
  *
85946
- * Only the names the settings reference lists are covered; its directory and
85947
- * Windows groups are introduced with "such as", so an unlisted member of those
86497
+ * Only the names the settings reference lists are covered; its directory,
86498
+ * Windows and sign-in groups are introduced with "such as", so an unlisted member of those
85948
86499
  * families is still written. Windows variable names are case-insensitive, so
85949
- * that group is matched without regard to case.
86500
+ * that group is matched without regard to case. `ALLUSERSPROFILE`,
86501
+ * `SystemDrive` and the `CommonProgramFiles` family are ignored there since
86502
+ * Claude Code v2.1.285.
85950
86503
  *
85951
86504
  * @see https://code.claude.com/docs/en/settings-reference#variables-claude-code-ignores-in-env
86505
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
85952
86506
  */
85953
86507
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85954
86508
  "BETA_TRACING_ENDPOINT",
@@ -85958,6 +86512,9 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85958
86512
  "CLAUDE_CODE_DISABLE_FILE_MENTIONS",
85959
86513
  "CLAUDE_CODE_ENABLE_TELEMETRY",
85960
86514
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
86515
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
86516
+ "CLAUDE_CODE_OAUTH_SCOPES",
86517
+ "CLAUDE_CODE_OAUTH_TOKEN",
85961
86518
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
85962
86519
  "CLAUDE_CODE_PLUGIN_SEED_DIR",
85963
86520
  "CLAUDE_CODE_PROCESS_WRAPPER",
@@ -85978,14 +86535,23 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85978
86535
  "TMPDIR"
85979
86536
  ]);
85980
86537
  const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES = /* @__PURE__ */ new Set([
86538
+ "ALLUSERSPROFILE",
86539
+ "COMMONPROGRAMW6432",
85981
86540
  "COMSPEC",
85982
86541
  "LOCALAPPDATA",
85983
86542
  "PATHEXT",
85984
86543
  "PROGRAMDATA",
85985
86544
  "PROGRAMW6432",
85986
86545
  "PSMODULEPATH",
86546
+ "SYSTEMDRIVE",
85987
86547
  "SYSTEMROOT"
85988
86548
  ]);
86549
+ /**
86550
+ * Windows name prefixes matched as whole families: `ProgramFiles*` and
86551
+ * `CommonProgramFiles*`. `ProgramW6432` and `CommonProgramW6432` are exact
86552
+ * names in the set above.
86553
+ */
86554
+ const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES = ["PROGRAMFILES", "COMMONPROGRAMFILES"];
85989
86555
  const CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES = [
85990
86556
  "_ENDPOINT",
85991
86557
  "_HEADERS",
@@ -86030,7 +86596,7 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
86030
86596
  function isProjectIgnoredEnvVariable(name, value) {
86031
86597
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_") || name.startsWith("CLAUDE_AX_")) return true;
86032
86598
  const upper = name.toUpperCase();
86033
- if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || upper.startsWith("PROGRAMFILES")) return true;
86599
+ if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES.some((prefix) => upper.startsWith(prefix))) return true;
86034
86600
  if (name.startsWith("OTEL_EXPORTER_OTLP_") && CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return true;
86035
86601
  if (Object.hasOwn(CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES, name)) {
86036
86602
  const offValues = CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES[name];
@@ -86062,7 +86628,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
86062
86628
  kept[name] = value;
86063
86629
  }
86064
86630
  if (ignoredEverywhere.length > 0) logger?.warn(`Claude Code permissions: Claude Code ignores ${ignoredEverywhere.map((name) => `'env.${name}'`).join(", ")} in every settings file, so ${ignoredEverywhere.length === 1 ? "it is" : "they are"} not written to ${relativeFilePath}. Set ${ignoredEverywhere.length === 1 ? "it" : "them"} in the environment Claude Code starts from instead, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
86065
- if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
86631
+ if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set the claude.ai sign-in, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
86066
86632
  return kept;
86067
86633
  }
86068
86634
  /**
@@ -86712,7 +87278,7 @@ const CODEBUDDY_RULE_LISTS = [
86712
87278
  "ask",
86713
87279
  "deny"
86714
87280
  ];
86715
- const CATCH_ALL_PATTERN$13 = "*";
87281
+ const CATCH_ALL_PATTERN$14 = "*";
86716
87282
  const MCP_CATEGORY = "mcp";
86717
87283
  const MCP_RULE_PREFIX = "mcp__";
86718
87284
  const ALL_MCP_RULE = "mcp__*";
@@ -86782,7 +87348,7 @@ function categoryOfRule$2(rule) {
86782
87348
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
86783
87349
  */
86784
87350
  function buildCodebuddyRule({ category, pattern, action, logger }) {
86785
- const catchAll = pattern === CATCH_ALL_PATTERN$13;
87351
+ const catchAll = pattern === CATCH_ALL_PATTERN$14;
86786
87352
  if (category === MCP_CATEGORY) {
86787
87353
  if (catchAll && action === "allow") {
86788
87354
  logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
@@ -86791,7 +87357,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
86791
87357
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
86792
87358
  }
86793
87359
  if (category === "*") {
86794
- if (catchAll) return CATCH_ALL_PATTERN$13;
87360
+ if (catchAll) return CATCH_ALL_PATTERN$14;
86795
87361
  logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
86796
87362
  return null;
86797
87363
  }
@@ -86809,7 +87375,7 @@ function parseCodebuddyRule(rule) {
86809
87375
  const { specifier } = splitCodebuddyRule(rule);
86810
87376
  if (specifier === void 0) return {
86811
87377
  category,
86812
- pattern: CATCH_ALL_PATTERN$13
87378
+ pattern: CATCH_ALL_PATTERN$14
86813
87379
  };
86814
87380
  const trimmed = specifier.trim();
86815
87381
  return trimmed === "" ? null : {
@@ -89135,8 +89701,8 @@ function mapBashActionToDecision(action) {
89135
89701
  // src/features/permissions/commandcode-permissions.ts
89136
89702
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
89137
89703
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
89138
- const CATCH_ALL_PATTERN$12 = "*";
89139
- const MCP_CANONICAL_PREFIX$7 = "mcp__";
89704
+ const CATCH_ALL_PATTERN$13 = "*";
89705
+ const MCP_CANONICAL_PREFIX$8 = "mcp__";
89140
89706
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
89141
89707
  const CATEGORY_TO_COMMANDCODE_TOOL = {
89142
89708
  bash: "Shell",
@@ -89181,10 +89747,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
89181
89747
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
89182
89748
  */
89183
89749
  function buildCommandcodeRule(category, pattern) {
89184
- const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
89185
- if (category.startsWith(MCP_CANONICAL_PREFIX$7)) return catchAll ? category : `${category}(${pattern})`;
89186
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$7}${pattern}`;
89187
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
89750
+ const catchAll = pattern === CATCH_ALL_PATTERN$13 || pattern === "";
89751
+ if (category.startsWith(MCP_CANONICAL_PREFIX$8)) return catchAll ? category : `${category}(${pattern})`;
89752
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$8}${pattern}`;
89753
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$13 : null;
89188
89754
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
89189
89755
  if (tool === void 0) return null;
89190
89756
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -89220,7 +89786,7 @@ function splitCommandcodeRule(rule) {
89220
89786
  }
89221
89787
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
89222
89788
  function isCatchAllSpecifier(inner) {
89223
- return inner === "" || inner === CATCH_ALL_PATTERN$12;
89789
+ return inner === "" || inner === CATCH_ALL_PATTERN$13;
89224
89790
  }
89225
89791
  /**
89226
89792
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -89254,34 +89820,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
89254
89820
  if (server.length === 0) return null;
89255
89821
  const everyServer = {
89256
89822
  category: "mcp",
89257
- pattern: CATCH_ALL_PATTERN$12,
89823
+ pattern: CATCH_ALL_PATTERN$13,
89258
89824
  notInAllow: true
89259
89825
  };
89260
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7)) {
89261
- if (server === CATCH_ALL_PATTERN$12) return everyServer;
89262
- if (server.includes(CATCH_ALL_PATTERN$12)) return null;
89826
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$8)) {
89827
+ if (server === CATCH_ALL_PATTERN$13) return everyServer;
89828
+ if (server.includes(CATCH_ALL_PATTERN$13)) return null;
89263
89829
  return {
89264
- category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$7}${server}` : `${MCP_CANONICAL_PREFIX$7}${remainder}`,
89265
- pattern: CATCH_ALL_PATTERN$12,
89830
+ category: rest === "" || rest === CATCH_ALL_PATTERN$13 ? `${MCP_CANONICAL_PREFIX$8}${server}` : `${MCP_CANONICAL_PREFIX$8}${remainder}`,
89831
+ pattern: CATCH_ALL_PATTERN$13,
89266
89832
  notInAllow: false
89267
89833
  };
89268
89834
  }
89269
- const category = `${MCP_CANONICAL_PREFIX$7}${remainder}`;
89270
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
89835
+ const category = `${MCP_CANONICAL_PREFIX$8}${remainder}`;
89836
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$13);
89271
89837
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
89272
89838
  category,
89273
89839
  pattern: inner,
89274
89840
  notInAllow: true
89275
89841
  } : null;
89276
- if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
89277
- if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
89278
- category: `${MCP_CANONICAL_PREFIX$7}${server}`,
89279
- pattern: CATCH_ALL_PATTERN$12,
89842
+ if (remainder === CATCH_ALL_PATTERN$13) return everyServer;
89843
+ if (rest === CATCH_ALL_PATTERN$13 && !server.includes(CATCH_ALL_PATTERN$13)) return {
89844
+ category: `${MCP_CANONICAL_PREFIX$8}${server}`,
89845
+ pattern: CATCH_ALL_PATTERN$13,
89280
89846
  notInAllow: true
89281
89847
  };
89282
89848
  return namesTool ? {
89283
89849
  category,
89284
- pattern: CATCH_ALL_PATTERN$12,
89850
+ pattern: CATCH_ALL_PATTERN$13,
89285
89851
  notInAllow: false
89286
89852
  } : null;
89287
89853
  }
@@ -89305,14 +89871,14 @@ function friendlyToolRule({ category, inner }) {
89305
89871
  if (category === "bash") {
89306
89872
  if (isCatchAllSpecifier(inner)) return {
89307
89873
  category,
89308
- pattern: CATCH_ALL_PATTERN$12,
89874
+ pattern: CATCH_ALL_PATTERN$13,
89309
89875
  notInAllow: false
89310
89876
  };
89311
89877
  const normalized = inner.trim().replace(/\s+/g, " ");
89312
89878
  if (normalized === "" || normalized === ":*") return null;
89313
89879
  return isCatchAllSpecifier(normalized) ? {
89314
89880
  category,
89315
- pattern: CATCH_ALL_PATTERN$12,
89881
+ pattern: CATCH_ALL_PATTERN$13,
89316
89882
  notInAllow: true
89317
89883
  } : {
89318
89884
  category,
@@ -89322,7 +89888,7 @@ function friendlyToolRule({ category, inner }) {
89322
89888
  }
89323
89889
  if (isCatchAllSpecifier(inner)) return {
89324
89890
  category,
89325
- pattern: CATCH_ALL_PATTERN$12,
89891
+ pattern: CATCH_ALL_PATTERN$13,
89326
89892
  notInAllow: false
89327
89893
  };
89328
89894
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -89341,13 +89907,13 @@ function friendlyToolRule({ category, inner }) {
89341
89907
  */
89342
89908
  function parseCommandcodeRule(rule) {
89343
89909
  const { tool, inner } = splitCommandcodeRule(rule);
89344
- if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
89910
+ if (tool === CATCH_ALL_PATTERN$13) return isCatchAllSpecifier(inner) ? {
89345
89911
  category: "*",
89346
- pattern: CATCH_ALL_PATTERN$12,
89912
+ pattern: CATCH_ALL_PATTERN$13,
89347
89913
  notInAllow: true
89348
89914
  } : null;
89349
89915
  const lowered = tool.toLowerCase();
89350
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$7)) return parseMcpCommandcodeRule({
89916
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$8)) return parseMcpCommandcodeRule({
89351
89917
  tool,
89352
89918
  inner
89353
89919
  });
@@ -89365,12 +89931,12 @@ function parseCommandcodeRule(rule) {
89365
89931
  */
89366
89932
  function isFoldedMcpToolNameRule({ rule, category }) {
89367
89933
  const { tool } = splitCommandcodeRule(rule);
89368
- return category.startsWith(MCP_CANONICAL_PREFIX$7) && !tool.startsWith(MCP_CANONICAL_PREFIX$7);
89934
+ return category.startsWith(MCP_CANONICAL_PREFIX$8) && !tool.startsWith(MCP_CANONICAL_PREFIX$8);
89369
89935
  }
89370
89936
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
89371
89937
  function scopedMcpRuleTool(rule) {
89372
89938
  const { tool, inner } = splitCommandcodeRule(rule);
89373
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$7) ? tool : null;
89939
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$8) ? tool : null;
89374
89940
  }
89375
89941
  /**
89376
89942
  * The string entries of a Command Code permission list. Command Code reads
@@ -89413,7 +89979,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
89413
89979
  }).filter((rule) => {
89414
89980
  const parsed = parseCommandcodeRule(rule);
89415
89981
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
89416
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$12})`);
89982
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$13})`);
89417
89983
  if (key !== "allow" && isStricterAction({
89418
89984
  action: key,
89419
89985
  existing: replacement
@@ -89470,7 +90036,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89470
90036
  const ranked = /* @__PURE__ */ new Map();
89471
90037
  const managedCategories = /* @__PURE__ */ new Set();
89472
90038
  for (const [category, rules] of Object.entries(permission)) {
89473
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
90039
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$13);
89474
90040
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
89475
90041
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
89476
90042
  for (const [pattern, action] of Object.entries(rules)) {
@@ -89539,7 +90105,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89539
90105
  function skippedAllowImportMessage({ rule, category }) {
89540
90106
  const { tool, inner } = splitCommandcodeRule(rule);
89541
90107
  if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
89542
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
90108
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$8) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
89543
90109
  return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
89544
90110
  }
89545
90111
  /**
@@ -89734,7 +90300,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
89734
90300
  }
89735
90301
  // src/features/permissions/continue-permissions.ts
89736
90302
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
89737
- const CATCH_ALL_PATTERN$11 = "*";
90303
+ const CATCH_ALL_PATTERN$12 = "*";
89738
90304
  /**
89739
90305
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
89740
90306
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -89742,7 +90308,7 @@ const CATCH_ALL_PATTERN$11 = "*";
89742
90308
  * the bare entry instead of being skipped as a pattern-specific rule.
89743
90309
  */
89744
90310
  function normalizeCatchAllPattern$1(pattern) {
89745
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90311
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$12 : pattern;
89746
90312
  }
89747
90313
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
89748
90314
  bash: "Bash",
@@ -89786,7 +90352,7 @@ function parseContinuePermissionEntry(entry) {
89786
90352
  if (toolName === "") return;
89787
90353
  return {
89788
90354
  toolName,
89789
- pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
90355
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$12 : pattern
89790
90356
  };
89791
90357
  }
89792
90358
  /**
@@ -89951,7 +90517,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
89951
90517
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89952
90518
  continue;
89953
90519
  }
89954
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
90520
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$12) {
89955
90521
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89956
90522
  continue;
89957
90523
  }
@@ -90007,13 +90573,116 @@ const AUTO_APPROVE_KEYS = {
90007
90573
  webfetch: "chat.tools.urls.autoApprove"
90008
90574
  };
90009
90575
  /**
90010
- * The workspace setting holding the starting permission level of new chat
90011
- * sessions, authored through the `copilot.defaultPermissionMode` override.
90576
+ * The workspace setting holding the starting permission level of new Local
90577
+ * (`local` harness) chat sessions, authored through the
90578
+ * `copilot.defaultPermissionMode` override. VS Code 1.144 narrowed it to Local
90579
+ * sessions; the Copilot harness reads {@link DEFAULT_CONFIGURATION_KEY}.
90012
90580
  *
90013
90581
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
90014
90582
  */
90015
90583
  const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
90016
90584
  /**
90585
+ * The workspace setting holding the starting `mode` (`interactive` / `plan` /
90586
+ * `autopilot`) and `approvals` (`manual` / `assisted` / `allowAll`) of new
90587
+ * agent sessions such as the Copilot harness, the default since VS Code 1.143.
90588
+ * It shipped in stable VS Code 1.144 as an object setting with
90589
+ * `additionalProperties: false`; older releases ignore it.
90590
+ *
90591
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
90592
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/common/constants.ts
90593
+ */
90594
+ const DEFAULT_CONFIGURATION_KEY = "chat.defaultConfiguration";
90595
+ /**
90596
+ * What each `copilot.defaultPermissionMode` value writes. `local` goes to
90597
+ * `chat.permissions.default`; `approvals` and `autopilot` go to
90598
+ * `chat.defaultConfiguration`. The Local picker has no Assisted level, so
90599
+ * `assisted` starts Local sessions with Default Permissions — the stricter
90600
+ * fallback. Autopilot is a permission level in the Local picker but a `mode`
90601
+ * in `chat.defaultConfiguration`, where it runs with every tool call approved.
90602
+ */
90603
+ const DEFAULT_PERMISSION_MODE_MAPPINGS = {
90604
+ default: {
90605
+ local: "default",
90606
+ approvals: "manual",
90607
+ autopilot: false
90608
+ },
90609
+ autoApprove: {
90610
+ local: "autoApprove",
90611
+ approvals: "allowAll",
90612
+ autopilot: false
90613
+ },
90614
+ autopilot: {
90615
+ local: "autopilot",
90616
+ approvals: "allowAll",
90617
+ autopilot: true
90618
+ },
90619
+ assisted: {
90620
+ local: "default",
90621
+ approvals: "assisted",
90622
+ autopilot: false
90623
+ }
90624
+ };
90625
+ /**
90626
+ * The `chat.defaultConfiguration` value for `mode`. Rulesync owns only whether
90627
+ * the mode is Autopilot: authoring Autopilot sets it, and any other value
90628
+ * removes any Autopilot mode in the file (whether an earlier run or a person
90629
+ * wrote it, since the two cannot be told apart), while an `interactive` or
90630
+ * `plan` mode is kept.
90631
+ */
90632
+ function buildDefaultConfigurationValue({ defaultPermissionMode, existingSettings }) {
90633
+ const { approvals, autopilot } = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode];
90634
+ const existing = existingSettings[DEFAULT_CONFIGURATION_KEY];
90635
+ const value = { ...isPlainObject(existing) ? existing : {} };
90636
+ if (autopilot) value.mode = "autopilot";
90637
+ else if (value.mode === "autopilot") delete value.mode;
90638
+ value.approvals = approvals;
90639
+ return value;
90640
+ }
90641
+ /**
90642
+ * The `copilot.defaultPermissionMode` values from least to most permissive,
90643
+ * used to settle a file whose two keys disagree.
90644
+ */
90645
+ const DEFAULT_PERMISSION_MODE_RANK = {
90646
+ default: 0,
90647
+ assisted: 1,
90648
+ autoApprove: 2,
90649
+ autopilot: 3
90650
+ };
90651
+ /**
90652
+ * The `copilot.defaultPermissionMode` a `chat.defaultConfiguration` value
90653
+ * stands for. Autopilot is recognized only together with `allowAll`, the pair
90654
+ * Rulesync writes for it: an Autopilot mode with stricter approvals is read
90655
+ * from `approvals` alone, so that regenerating never escalates it to Allow
90656
+ * All. An object without `approvals` is read as `default`, since VS Code then
90657
+ * starts sessions with Manual permissions.
90658
+ */
90659
+ function readDefaultConfigurationMode(configuration) {
90660
+ if (!isPlainObject(configuration)) return;
90661
+ switch (configuration.approvals) {
90662
+ case void 0:
90663
+ case "manual": return "default";
90664
+ case "assisted": return "assisted";
90665
+ case "allowAll": return configuration.mode === "autopilot" ? "autopilot" : "autoApprove";
90666
+ default: return;
90667
+ }
90668
+ }
90669
+ /**
90670
+ * Read `copilot.defaultPermissionMode` back from the settings. When the two
90671
+ * keys agree — `chat.permissions.default` holds the Local level Rulesync
90672
+ * writes for the `chat.defaultConfiguration` value — the latter is returned,
90673
+ * since only it can say `assisted`. When they disagree, the less permissive of
90674
+ * the two is returned, so regenerating never raises either harness's starting
90675
+ * permissions. Either key alone is read as it stands.
90676
+ */
90677
+ function readDefaultPermissionMode(settings) {
90678
+ const fromConfiguration = readDefaultConfigurationMode(settings[DEFAULT_CONFIGURATION_KEY]);
90679
+ const localValue = settings[DEFAULT_PERMISSION_MODE_KEY$1];
90680
+ const fromLocal = COPILOT_LOCAL_PERMISSION_LEVELS.find((level) => level === localValue);
90681
+ if (fromConfiguration === void 0 || fromLocal === void 0) return fromConfiguration ?? fromLocal;
90682
+ if (DEFAULT_PERMISSION_MODE_MAPPINGS[fromConfiguration].local === fromLocal) return fromConfiguration;
90683
+ return DEFAULT_PERMISSION_MODE_RANK[fromLocal] < DEFAULT_PERMISSION_MODE_RANK[fromConfiguration] ? fromLocal : fromConfiguration;
90684
+ }
90685
+ /**
90017
90686
  * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
90018
90687
  * at path `a.b` is written to `chat.agent.sandbox.a.b`.
90019
90688
  */
@@ -90200,9 +90869,6 @@ function readSandboxFromSettings(settings) {
90200
90869
  }
90201
90870
  return Object.keys(sandbox).length > 0 ? sandbox : void 0;
90202
90871
  }
90203
- function isCopilotDefaultPermissionMode(value) {
90204
- return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
90205
- }
90206
90872
  function asAutoApproveMap(value) {
90207
90873
  if (!isPlainObject(value)) return {};
90208
90874
  const result = {};
@@ -90246,9 +90912,11 @@ function buildAutoApproveValue({ category, rules }) {
90246
90912
  * `false`. A key whose canonical category is absent entirely
90247
90913
  * is left untouched, so authoring only `bash` rules never disturbs a
90248
90914
  * hand-written edits or urls map.
90249
- * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
90250
- * (the starting permission level of new chat sessions); without it the key is
90251
- * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
90915
+ * The `copilot.defaultPermissionMode` override writes both
90916
+ * `chat.permissions.default` (Local sessions) and `chat.defaultConfiguration`
90917
+ * (Copilot-harness sessions, VS Code 1.144+; see
90918
+ * {@link DEFAULT_PERMISSION_MODE_MAPPINGS}); without it both keys are left
90919
+ * untouched. The `copilot.sandbox` override writes the Agent Host sandbox
90252
90920
  * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
90253
90921
  * warns about every value it writes that loosens the sandbox.
90254
90922
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
@@ -90299,17 +90967,24 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90299
90967
  rules
90300
90968
  });
90301
90969
  }
90970
+ const existingSettings = () => parseSharedConfig({
90971
+ format: "jsonc",
90972
+ fileContent: existingContent,
90973
+ filePath
90974
+ });
90302
90975
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
90303
- if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY$1] = defaultPermissionMode;
90976
+ if (defaultPermissionMode !== void 0) {
90977
+ patch[DEFAULT_PERMISSION_MODE_KEY$1] = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode].local;
90978
+ patch[DEFAULT_CONFIGURATION_KEY] = buildDefaultConfigurationValue({
90979
+ defaultPermissionMode,
90980
+ existingSettings: existingSettings()
90981
+ });
90982
+ }
90304
90983
  const sandbox = config.copilot?.sandbox;
90305
90984
  if (sandbox !== void 0) {
90306
90985
  Object.assign(patch, buildSandboxPatch({
90307
90986
  sandbox,
90308
- existingSettings: parseSharedConfig({
90309
- format: "jsonc",
90310
- fileContent: existingContent,
90311
- filePath
90312
- })
90987
+ existingSettings: existingSettings()
90313
90988
  }));
90314
90989
  warnOnTrustAffectingEntries({
90315
90990
  toolLabel: COPILOT_TOOL_LABEL,
@@ -90356,10 +91031,10 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90356
91031
  for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
90357
91032
  if (Object.keys(rules).length > 0) permission[category] = rules;
90358
91033
  }
90359
- const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY$1];
91034
+ const defaultPermissionMode = readDefaultPermissionMode(settings);
90360
91035
  const sandbox = readSandboxFromSettings(settings);
90361
91036
  const copilot = {
90362
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
91037
+ ...defaultPermissionMode !== void 0 && { defaultPermissionMode },
90363
91038
  ...sandbox !== void 0 && { sandbox }
90364
91039
  };
90365
91040
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -90557,14 +91232,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
90557
91232
  }
90558
91233
  };
90559
91234
  // src/features/permissions/crush-permissions.ts
90560
- const CATCH_ALL_PATTERN$10 = "*";
91235
+ const CATCH_ALL_PATTERN$11 = "*";
90561
91236
  /**
90562
91237
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
90563
91238
  * argument patterns at all, so it is the only pattern a rule can carry and
90564
91239
  * still be written.
90565
91240
  */
90566
91241
  function normalizeCatchAllPattern(pattern) {
90567
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
91242
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90568
91243
  }
90569
91244
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90570
91245
  bash: "bash",
@@ -90576,11 +91251,11 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90576
91251
  webfetch: "fetch"
90577
91252
  };
90578
91253
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
90579
- const MCP_CANONICAL_PREFIX$6 = "mcp__";
91254
+ const MCP_CANONICAL_PREFIX$7 = "mcp__";
90580
91255
  const MCP_CRUSH_PREFIX = "mcp_";
90581
91256
  function toCrushToolName(canonical) {
90582
91257
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
90583
- if (canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
91258
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX$7) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
90584
91259
  return canonical;
90585
91260
  }
90586
91261
  function toCanonicalToolName$5(crushName) {
@@ -90754,7 +91429,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
90754
91429
  for (const [rawPattern, action] of Object.entries(rules)) {
90755
91430
  if (isPrototypePollutionKey(rawPattern)) continue;
90756
91431
  const pattern = normalizeCatchAllPattern(rawPattern);
90757
- if (pattern === CATCH_ALL_PATTERN$10) {
91432
+ if (pattern === CATCH_ALL_PATTERN$11) {
90758
91433
  catchAllAction = action;
90759
91434
  continue;
90760
91435
  }
@@ -90827,11 +91502,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
90827
91502
  };
90828
91503
  for (const entry of disabled) {
90829
91504
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
90830
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
91505
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] = "deny";
90831
91506
  }
90832
91507
  for (const entry of allowed) {
90833
91508
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
90834
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
91509
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] ??= "allow";
90835
91510
  }
90836
91511
  return { permission };
90837
91512
  }
@@ -90877,13 +91552,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
90877
91552
  WebFetch: "webfetch",
90878
91553
  Mcp: "mcp"
90879
91554
  };
90880
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
91555
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
90881
91556
  /**
90882
91557
  * Returns true if the canonical category is the per-tool MCP form
90883
91558
  * `mcp__<server>__<tool>`.
90884
91559
  */
90885
91560
  function isMcpScopedCategory(canonical) {
90886
- return canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5;
91561
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5;
90887
91562
  }
90888
91563
  function toCursorType(canonical) {
90889
91564
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -90917,7 +91592,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
90917
91592
  if (match) {
90918
91593
  const server = match[1] ?? "*";
90919
91594
  const tool = match[2] ?? "*";
90920
- return `${MCP_CANONICAL_PREFIX$5}${server}__${tool}`;
91595
+ return `${MCP_CANONICAL_PREFIX$6}${server}__${tool}`;
90921
91596
  }
90922
91597
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
90923
91598
  }
@@ -91179,7 +91854,7 @@ function convertCursorToRulesyncPermissions(params) {
91179
91854
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
91180
91855
  const canonical = toCanonicalCategory$1(type, pattern);
91181
91856
  if (!permission[canonical]) permission[canonical] = {};
91182
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$5) ? "*" : pattern;
91857
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$6) ? "*" : pattern;
91183
91858
  permission[canonical][canonicalPattern] = action;
91184
91859
  }
91185
91860
  };
@@ -92678,7 +93353,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
92678
93353
  // src/features/permissions/goose-permissions.ts
92679
93354
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
92680
93355
  const GOOSE_USER_KEY = "user";
92681
- const CATCH_ALL_PATTERN$9 = "*";
93356
+ const CATCH_ALL_PATTERN$10 = "*";
92682
93357
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
92683
93358
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
92684
93359
  shell: ["bash"],
@@ -92827,7 +93502,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
92827
93502
  const assigned = /* @__PURE__ */ new Map();
92828
93503
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
92829
93504
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
92830
- for (const [pattern, action] of Object.entries(rules)) if (pattern !== CATCH_ALL_PATTERN$9) logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
93505
+ for (const [pattern, action] of Object.entries(rules)) if (pattern !== CATCH_ALL_PATTERN$10) logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
92831
93506
  const { action, downgraded } = failSafeCatchAllAction({
92832
93507
  rules,
92833
93508
  allToolsRules: category === "*" ? void 0 : config.permission["*"]
@@ -92860,7 +93535,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
92860
93535
  const action = GOOSE_LIST_TO_ACTION[key];
92861
93536
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
92862
93537
  permission[category] ??= {};
92863
- permission[category][CATCH_ALL_PATTERN$9] = action;
93538
+ permission[category][CATCH_ALL_PATTERN$10] = action;
92864
93539
  }
92865
93540
  }
92866
93541
  return { permission };
@@ -92870,8 +93545,8 @@ const GROKCLI_UI_KEY = "ui";
92870
93545
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
92871
93546
  const GROKCLI_PERMISSION_KEY = "permission";
92872
93547
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
92873
- const CATCH_ALL_PATTERN$8 = "*";
92874
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
93548
+ const CATCH_ALL_PATTERN$9 = "*";
93549
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
92875
93550
  const CATEGORY_TO_GROK_TOOL = {
92876
93551
  bash: "Bash",
92877
93552
  read: "Read",
@@ -92903,6 +93578,25 @@ const GROK_TOOL_TO_CATEGORY_LOWER = {
92903
93578
  };
92904
93579
  const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
92905
93580
  const GROKCLI_PERMISSION_RULES_KEY = "rules";
93581
+ const GROK_PATH_PATTERN_CATEGORIES = /* @__PURE__ */ new Set([
93582
+ "read",
93583
+ "edit",
93584
+ "write",
93585
+ "grep",
93586
+ "glob"
93587
+ ]);
93588
+ const CLAUDE_ABSOLUTE_PATH_PREFIX = "//";
93589
+ /**
93590
+ * Rewrite a canonical path pattern into the anchor Grok understands. A
93591
+ * Claude-style `//abs` absolute pattern becomes `/abs`; without this the rule
93592
+ * would match nothing on Grok, so a `deny` or `ask` rule would silently give
93593
+ * no protection. Every other pattern is returned unchanged.
93594
+ */
93595
+ function toGrokPathPattern(category, pattern) {
93596
+ if (!GROK_PATH_PATTERN_CATEGORIES.has(category)) return pattern;
93597
+ if (!pattern.startsWith(CLAUDE_ABSOLUTE_PATH_PREFIX)) return pattern;
93598
+ return `/${pattern.replace(/^\/+/, "")}`;
93599
+ }
92906
93600
  /**
92907
93601
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
92908
93602
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -92913,18 +93607,20 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
92913
93607
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
92914
93608
  * concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
92915
93609
  * Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
92916
- * under it has no Grok spelling and returns `null`.
93610
+ * under it has no Grok spelling and returns `null`. A Claude-style `//abs`
93611
+ * path pattern on a file tool is rewritten to Grok's `/abs` (see
93612
+ * `toGrokPathPattern`).
92917
93613
  */
92918
93614
  function buildGrokEntry(category, pattern) {
92919
- if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
92920
- if (category.startsWith(MCP_CANONICAL_PREFIX$4)) {
93615
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$9 ? GROK_ALL_TOOLS_ENTRY : null;
93616
+ if (category.startsWith(MCP_CANONICAL_PREFIX$5)) {
92921
93617
  const remainder = category.slice(5);
92922
93618
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
92923
93619
  }
92924
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
93620
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
92925
93621
  const tool = CATEGORY_TO_GROK_TOOL[category];
92926
93622
  if (tool === void 0) return null;
92927
- return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
93623
+ return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
92928
93624
  }
92929
93625
  /**
92930
93626
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -92946,21 +93642,21 @@ function parseGrokEntry(entry) {
92946
93642
  }
92947
93643
  if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
92948
93644
  category: "*",
92949
- pattern: CATCH_ALL_PATTERN$8
93645
+ pattern: CATCH_ALL_PATTERN$9
92950
93646
  };
92951
- if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$4)) return parseClaudeStyleMcpEntry(tool);
93647
+ if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$5)) return parseClaudeStyleMcpEntry(tool);
92952
93648
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
92953
- category: `${MCP_CANONICAL_PREFIX$4}${inner}`,
92954
- pattern: CATCH_ALL_PATTERN$8
93649
+ category: `${MCP_CANONICAL_PREFIX$5}${inner}`,
93650
+ pattern: CATCH_ALL_PATTERN$9
92955
93651
  } : {
92956
93652
  category: "mcp",
92957
- pattern: CATCH_ALL_PATTERN$8
93653
+ pattern: CATCH_ALL_PATTERN$9
92958
93654
  };
92959
93655
  const category = GROK_TOOL_TO_CATEGORY[tool];
92960
93656
  if (category === void 0) return null;
92961
93657
  return {
92962
93658
  category,
92963
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
93659
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$9
92964
93660
  };
92965
93661
  }
92966
93662
  /**
@@ -92977,14 +93673,14 @@ function parseGrokEntry(entry) {
92977
93673
  function parseClaudeStyleMcpEntry(entry) {
92978
93674
  const rest = entry.slice(5);
92979
93675
  if (rest.length === 0) return null;
92980
- if (rest === CATCH_ALL_PATTERN$8) return {
93676
+ if (rest === CATCH_ALL_PATTERN$9) return {
92981
93677
  category: "mcp",
92982
- pattern: CATCH_ALL_PATTERN$8
93678
+ pattern: CATCH_ALL_PATTERN$9
92983
93679
  };
92984
93680
  const address = rest.includes("__") ? rest : `${rest}__*`;
92985
93681
  return {
92986
- category: `${MCP_CANONICAL_PREFIX$4}${address}`,
92987
- pattern: CATCH_ALL_PATTERN$8
93682
+ category: `${MCP_CANONICAL_PREFIX$5}${address}`,
93683
+ pattern: CATCH_ALL_PATTERN$9
92988
93684
  };
92989
93685
  }
92990
93686
  /**
@@ -93005,20 +93701,20 @@ function parseGrokRule(rule) {
93005
93701
  if (typeof tool !== "string") return null;
93006
93702
  if (pattern !== void 0 && typeof pattern !== "string") return null;
93007
93703
  const trimmedPattern = pattern?.trim() ?? "";
93008
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
93704
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$9;
93009
93705
  const lowerTool = tool.trim().toLowerCase();
93010
- if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
93706
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93011
93707
  category: "*",
93012
- pattern: CATCH_ALL_PATTERN$8,
93708
+ pattern: CATCH_ALL_PATTERN$9,
93013
93709
  action
93014
93710
  } : null;
93015
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
93711
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93016
93712
  category: "mcp",
93017
- pattern: CATCH_ALL_PATTERN$8,
93713
+ pattern: CATCH_ALL_PATTERN$9,
93018
93714
  action
93019
93715
  } : {
93020
- category: `${MCP_CANONICAL_PREFIX$4}${resolvedPattern}`,
93021
- pattern: CATCH_ALL_PATTERN$8,
93716
+ category: `${MCP_CANONICAL_PREFIX$5}${resolvedPattern}`,
93717
+ pattern: CATCH_ALL_PATTERN$9,
93022
93718
  action
93023
93719
  };
93024
93720
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -93168,7 +93864,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
93168
93864
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
93169
93865
  }
93170
93866
  const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
93171
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$8]: legacyModeAction(parsed) } } };
93867
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$9]: legacyModeAction(parsed) } } };
93172
93868
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
93173
93869
  }
93174
93870
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
@@ -93353,6 +94049,195 @@ function ensureCategory(permission, category) {
93353
94049
  function removeEmptyCategories(permission) {
93354
94050
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
93355
94051
  }
94052
+ /** The prefix Hermes's MCP trust gate keys its persisted "Always" answers with. */
94053
+ const MCP_ALLOWLIST_PREFIX = "mcp:";
94054
+ const MCP_CANONICAL_PREFIX$4 = "mcp__";
94055
+ const CATCH_ALL_PATTERN$8 = "*";
94056
+ /**
94057
+ * Whether a `command_allowlist` entry is an MCP trust-gate grant rather than a
94058
+ * shell-command pattern. Since Hermes Agent v0.22.6, answering **Always** to
94059
+ * the approval prompt of a `trust: untrusted` MCP server appends
94060
+ * `mcp:<server>/<tool>` to `command_allowlist` (`_trust_approval_key` in
94061
+ * `tools/mcp_tool_handlers.py`), and the gate looks the key up by exact set
94062
+ * membership, so no `mcp:` entry is ever a command.
94063
+ */
94064
+ function isMcpAllowlistEntry(entry) {
94065
+ return entry.startsWith(MCP_ALLOWLIST_PREFIX);
94066
+ }
94067
+ /**
94068
+ * The canonical category (`mcp__<server>__<tool>`) a Hermes `mcp:<server>/<tool>`
94069
+ * grant stands for, or `null` when it has none: a malformed entry, a server
94070
+ * name containing `__` (the canonical name would read back as another server),
94071
+ * or a `*` in either name (Hermes compares the key literally, while a canonical
94072
+ * `*` is a wildcard).
94073
+ */
94074
+ function mcpAllowlistEntryToCategory(entry) {
94075
+ const parsed = parseMcpAllowlistEntry(entry);
94076
+ if (parsed === null) return null;
94077
+ const { server, tool } = parsed;
94078
+ if (server.includes("__") || server.includes(CATCH_ALL_PATTERN$8)) return null;
94079
+ if (tool.includes(CATCH_ALL_PATTERN$8)) return null;
94080
+ return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
94081
+ }
94082
+ /** The server and tool an `mcp:<server>/<tool>` entry names, or `null` when it is malformed. */
94083
+ function parseMcpAllowlistEntry(entry) {
94084
+ if (!isMcpAllowlistEntry(entry)) return null;
94085
+ const rest = entry.slice(4);
94086
+ const slash = rest.indexOf("/");
94087
+ const server = slash === -1 ? "" : rest.slice(0, slash);
94088
+ const tool = slash === -1 ? "" : rest.slice(slash + 1);
94089
+ return server.length === 0 || tool.length === 0 ? null : {
94090
+ server,
94091
+ tool
94092
+ };
94093
+ }
94094
+ /**
94095
+ * The server and tool a canonical `mcp__<server>__<tool>` category names, or
94096
+ * `null` when it names no single tool (a whole server, a wildcard, or the bare
94097
+ * `mcp` category). Only such a category can become one Hermes grant.
94098
+ */
94099
+ function parseMcpToolCategory(category) {
94100
+ if (!category.startsWith(MCP_CANONICAL_PREFIX$4)) return null;
94101
+ const remainder = category.slice(5);
94102
+ const separator = remainder.indexOf("__");
94103
+ if (separator <= 0) return null;
94104
+ const server = remainder.slice(0, separator);
94105
+ const tool = remainder.slice(separator + 2);
94106
+ if (tool.length === 0 || server.includes("/") || server.includes(CATCH_ALL_PATTERN$8) || tool.includes(CATCH_ALL_PATTERN$8)) return null;
94107
+ return {
94108
+ server,
94109
+ tool
94110
+ };
94111
+ }
94112
+ function isMcpCategory$1(category) {
94113
+ return category === "mcp" || category.startsWith(MCP_CANONICAL_PREFIX$4);
94114
+ }
94115
+ function isRestricting(action) {
94116
+ return action === "deny" || action === "ask";
94117
+ }
94118
+ /** The first `deny` or `ask` rule of a category, as `<category>: <pattern> (<action>)`. */
94119
+ function firstRestrictingRule(category, rules, covers = () => true) {
94120
+ const found = Object.entries(rules ?? {}).find(([pattern, action]) => isRestricting(action) && covers(pattern));
94121
+ return found ? `${category}: ${found[0]} (${found[1]})` : null;
94122
+ }
94123
+ /**
94124
+ * Whether an MCP name glob (the remainder of an `mcp__...` category, or a
94125
+ * pattern of the bare `mcp` category) covers `server`'s tool `tool`: it names
94126
+ * the tool (`github__delete_repo`, `github__*`, `*__delete_repo`, `*`) or the
94127
+ * whole server (`github`).
94128
+ */
94129
+ function mcpNameGlobCovers(glob, server, tool) {
94130
+ return glob === `${server}__${tool}` || glob === server || matchesGlob(glob, `${server}__${tool}`) || matchesGlob(glob, server);
94131
+ }
94132
+ /**
94133
+ * The `deny` or `ask` rule that outranks an allow on `server`'s tool `tool`, or
94134
+ * `null` when none does: any one under the all-tools `*` category (a rule there
94135
+ * applies to every tool, so even a narrower pattern restricts some of the
94136
+ * tool's calls), one under the bare `mcp` category whose pattern covers the
94137
+ * tool, or any one in an `mcp__...` category whose name covers the tool — its
94138
+ * own category included, since a deny or ask scoped to some of its calls still
94139
+ * forbids approving all of them. A Hermes grant approves every call of the
94140
+ * tool and Hermes has no ask tier, so such an allow is withheld, the same way
94141
+ * a `bash` allow a stricter rule covers is.
94142
+ */
94143
+ function findMcpToolRestriction(permission, server, tool) {
94144
+ for (const [category, rules] of Object.entries(permission)) {
94145
+ let found = null;
94146
+ if (category === "*") found = firstRestrictingRule(category, rules);
94147
+ else if (category === "mcp") found = firstRestrictingRule(category, rules, (pattern) => mcpNameGlobCovers(pattern, server, tool));
94148
+ else if (category.startsWith(MCP_CANONICAL_PREFIX$4) && mcpNameGlobCovers(category.slice(5), server, tool)) found = firstRestrictingRule(category, rules);
94149
+ if (found !== null) return found;
94150
+ }
94151
+ return null;
94152
+ }
94153
+ function isMcpToolRestricted(permission, server, tool) {
94154
+ return findMcpToolRestriction(permission, server, tool) !== null;
94155
+ }
94156
+ /**
94157
+ * Translate the canonical MCP `allow` rules into Hermes trust-gate grants: a
94158
+ * `*` allow on `mcp__<server>__<tool>` becomes `mcp:<server>/<tool>`. Every
94159
+ * other MCP allow (a whole server, a wildcard, a narrower pattern) has no
94160
+ * Hermes spelling, and an allow a deny or ask covers is withheld.
94161
+ */
94162
+ function collectMcpAllowlistEntries(permission) {
94163
+ const entries = [];
94164
+ const withheldCategories = [];
94165
+ const unexpressedRules = [];
94166
+ for (const [category, rules] of Object.entries(permission)) {
94167
+ if (!isMcpCategory$1(category)) continue;
94168
+ const parsed = parseMcpToolCategory(category);
94169
+ for (const pattern of patternsByAction(rules, "allow")) if (parsed === null || pattern !== CATCH_ALL_PATTERN$8) unexpressedRules.push(`${category}: ${pattern}`);
94170
+ else {
94171
+ const restriction = findMcpToolRestriction(permission, parsed.server, parsed.tool);
94172
+ if (restriction === null) entries.push(`${MCP_ALLOWLIST_PREFIX}${parsed.server}/${parsed.tool}`);
94173
+ else withheldCategories.push(`${category} (covered by ${restriction})`);
94174
+ }
94175
+ }
94176
+ return {
94177
+ entries,
94178
+ withheldCategories,
94179
+ unexpressedRules
94180
+ };
94181
+ }
94182
+ function warnAboutUnwrittenMcpAllows({ withheldCategories, unexpressedRules, logger }) {
94183
+ if (withheldCategories.length > 0) warnWithFallback(logger, `Hermes Agent's command_allowlist was not given the MCP allow rule(s) for ${withheldCategories.join(", ")}: a deny or ask rule covers each, a Hermes grant approves every call of the tool, and Hermes has no ask tier.`);
94184
+ if (unexpressedRules.length > 0) warnWithFallback(logger, `Hermes Agent grants MCP tools one at a time (command_allowlist entries 'mcp:<server>/<tool>'), so only a '*' allow on an 'mcp__<server>__<tool>' category can be written; the MCP allow rule(s) ${unexpressedRules.join(", ")} were skipped and survive only in the permissions.rulesync round-trip block.`);
94185
+ }
94186
+ /**
94187
+ * Make the native `mcp:<server>/<tool>` grants authoritative for the canonical
94188
+ * single-tool MCP allows they can speak for. An allow the generator withheld
94189
+ * because a deny or ask covers it was never written, so its absence
94190
+ * is not a retraction and it is kept, as is every MCP allow with no Hermes
94191
+ * spelling. A grant with no canonical spelling stays only in Hermes, where
94192
+ * generation keeps it (see `setFileContent`).
94193
+ */
94194
+ function reconcileMcpAllowlist({ permission, mcpEntries, logger }) {
94195
+ for (const [category, rules] of Object.entries(permission)) {
94196
+ const parsed = parseMcpToolCategory(category);
94197
+ if (parsed !== null && rules[CATCH_ALL_PATTERN$8] === "allow" && !isMcpToolRestricted(permission, parsed.server, parsed.tool)) delete rules[CATCH_ALL_PATTERN$8];
94198
+ }
94199
+ const keptRestrictions = [];
94200
+ for (const entry of mcpEntries) {
94201
+ const category = mcpAllowlistEntryToCategory(entry);
94202
+ if (category === null) continue;
94203
+ const rules = ensureCategory(permission, category);
94204
+ if (isRestricting(rules[CATCH_ALL_PATTERN$8])) {
94205
+ keptRestrictions.push(entry);
94206
+ continue;
94207
+ }
94208
+ rules[CATCH_ALL_PATTERN$8] = "allow";
94209
+ }
94210
+ if (keptRestrictions.length > 0) warnWithFallback(logger, `Hermes Agent's command_allowlist grants ${keptRestrictions.join(", ")}, but the permissions.rulesync block denies or asks for those MCP tools; the canonical rule was kept and the grant was not imported.`);
94211
+ }
94212
+ /**
94213
+ * The `mcp:` grants in an existing `command_allowlist` that rulesync does not
94214
+ * manage and must keep when it rewrites the list: Hermes appends one whenever
94215
+ * the user answers **Always** on an untrusted MCP server, so dropping it would
94216
+ * silently revoke the grant. A grant is managed — and so retracted with its
94217
+ * canonical rule — when the previous or the new canonical block has a rule
94218
+ * for its `mcp__<server>__<tool>` category, or when the new block restricts
94219
+ * the tool in any way.
94220
+ */
94221
+ function unmanagedMcpAllowlistEntries({ existing, previousPermission, nextPermission, nextAllowlist }) {
94222
+ return (isStringArray(existing.command_allowlist) ? existing.command_allowlist : []).filter((entry) => {
94223
+ if (!isMcpAllowlistEntry(entry) || nextAllowlist.includes(entry)) return false;
94224
+ const parsed = parseMcpAllowlistEntry(entry);
94225
+ if (parsed !== null && isMcpToolRestricted(nextPermission, parsed.server, parsed.tool)) return false;
94226
+ const category = mcpAllowlistEntryToCategory(entry);
94227
+ return category === null || !Object.hasOwn(previousPermission, category) && !Object.hasOwn(nextPermission, category);
94228
+ });
94229
+ }
94230
+ /** The validated canonical `permission` block of a config's `permissions.rulesync` blob. */
94231
+ function canonicalPermissionOf(config) {
94232
+ const parsed = RulesyncPermissionsFileSchema.safeParse({ permission: provenancePermissionOf(config) });
94233
+ return parsed.success ? parsed.data.permission : {};
94234
+ }
94235
+ /** The canonical `permission` block of a config's `permissions.rulesync` blob, if any. */
94236
+ function provenancePermissionOf(config) {
94237
+ const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
94238
+ const provenance = isRecord(permissionsRoot.rulesync) ? permissionsRoot.rulesync : {};
94239
+ return isRecord(provenance.permission) ? provenance.permission : {};
94240
+ }
93356
94241
  /**
93357
94242
  * Make the native `command_allowlist` authoritative for the `bash` allow rules
93358
94243
  * it can speak for, and for those only. The allowlist is a list of shell-command
@@ -93370,8 +94255,9 @@ function reconcileCommandAllowlist({ permission, commandAllowlist }) {
93370
94255
  writesAllToolsDeny: false
93371
94256
  });
93372
94257
  const withheld = new Set(shadowedAllowPatterns);
94258
+ const isWithheld = (pattern) => withheld.has(pattern) || isMcpAllowlistEntry(pattern);
93373
94259
  const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
93374
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
94260
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !isWithheld(pattern)) delete rules[pattern];
93375
94261
  for (const pattern of commandAllowlist) rules[pattern] = "allow";
93376
94262
  }
93377
94263
  /**
@@ -93459,17 +94345,31 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93459
94345
  return true;
93460
94346
  }
93461
94347
  setFileContent(fileContent) {
94348
+ const patch = parseSharedConfig({
94349
+ format: "yaml",
94350
+ fileContent: this.fileContent
94351
+ });
94352
+ if (isStringArray(patch.command_allowlist)) {
94353
+ const existing = parseSharedConfig({
94354
+ format: "yaml",
94355
+ fileContent
94356
+ });
94357
+ const kept = unmanagedMcpAllowlistEntries({
94358
+ existing,
94359
+ previousPermission: provenancePermissionOf(existing),
94360
+ nextPermission: canonicalPermissionOf(patch),
94361
+ nextAllowlist: patch.command_allowlist
94362
+ });
94363
+ if (kept.length > 0) patch.command_allowlist = [...patch.command_allowlist, ...kept];
94364
+ }
93462
94365
  this.fileContent = applySharedConfigPatch({
93463
94366
  fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
93464
94367
  feature: "permissions",
93465
94368
  existingContent: fileContent,
93466
- patch: parseSharedConfig({
93467
- format: "yaml",
93468
- fileContent: this.fileContent
93469
- })
94369
+ patch
93470
94370
  });
93471
94371
  }
93472
- toRulesyncPermissions() {
94372
+ toRulesyncPermissions({ logger } = {}) {
93473
94373
  const config = parseSharedConfig({
93474
94374
  format: "yaml",
93475
94375
  fileContent: this.getFileContent()
@@ -93481,9 +94381,15 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93481
94381
  }) : rawProvenance);
93482
94382
  const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
93483
94383
  const permission = clonePermissionBlock(provenance.permission);
94384
+ const commandAllowlist = isStringArray(config.command_allowlist) ? config.command_allowlist : [];
93484
94385
  reconcileCommandAllowlist({
93485
94386
  permission,
93486
- commandAllowlist: isStringArray(config.command_allowlist) ? config.command_allowlist : []
94387
+ commandAllowlist: commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry))
94388
+ });
94389
+ reconcileMcpAllowlist({
94390
+ permission,
94391
+ mcpEntries: commandAllowlist.filter(isMcpAllowlistEntry),
94392
+ logger
93487
94393
  });
93488
94394
  const approvals = isRecord(config.approvals) ? config.approvals : {};
93489
94395
  reconcileNativeDenies({
@@ -93541,8 +94447,16 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93541
94447
  logger
93542
94448
  });
93543
94449
  const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
94450
+ const mcpAllows = collectMcpAllowlistEntries(permissionBlock);
94451
+ warnAboutUnwrittenMcpAllows({
94452
+ ...mcpAllows,
94453
+ logger
94454
+ });
94455
+ const bashMcpAllows = commandAllowlist.filter(isMcpAllowlistEntry);
94456
+ if (bashMcpAllows.length > 0) warnWithFallback(logger, `Hermes Agent reads 'mcp:' entries in command_allowlist as MCP tool grants, so the bash allow rule(s) ${bashMcpAllows.join(", ")} were skipped; write them as a '*' allow on an 'mcp__<server>__<tool>' category instead.`);
94457
+ const allowlist = [...commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry)), ...mcpAllows.entries];
93544
94458
  let config = {};
93545
- if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
94459
+ if (allowlist.length > 0) config.command_allowlist = allowlist;
93546
94460
  if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
93547
94461
  if (webfetchDeny.length > 0) config.security = { website_blocklist: {
93548
94462
  enabled: true,
@@ -100383,6 +101297,7 @@ const TAKT_SECURITY_POLICIES = {
100383
101297
  ],
100384
101298
  workflow_runtime_prepare: ["custom_scripts"],
100385
101299
  workflow_command_gates: ["custom_scripts"],
101300
+ workflow_system_commands: ["custom_scripts"],
100386
101301
  sync_conflict_resolver: ["auto_approve_tools"],
100387
101302
  allow_git_hooks: null,
100388
101303
  allow_git_filters: null
@@ -107352,4 +108267,4 @@ async function importChecksCore(params) {
107352
108267
  }
107353
108268
  export { RulesyncPermissions as $, formatError as $n, isSymlink as $t, CODEBUDDY_DIR as A, MAX_FILE_SIZE as An, withFallbackLoggerTarget as At, CLAUDECODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Bn, assertTreeContainsNoSymlinks as Bt, QWENCODE_LOCAL_RULE_FILE_NAME as C, PACKAGING_TOOL_TARGETS as Cn, getSourceTransport as Ct, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as D, DEPRECATED_FEATURE_REPLACEMENTS as Dn, WarningCollectingLogger as Dt, FACTORYDROID_DIR as E, ALL_FEATURES_WITH_WILDCARD as En, JsonLogger as Et, groupSpellingsByCaseFoldedIdentity as F, RULESYNC_CONFIG_SCHEMA_URL as Fn, PathOutsideRootError as Ft, toInstructionBudgetFile as G, RULESYNC_PERMISSIONS_SCHEMA_URL as Gn, ensureDir as Gt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as H, RULESYNC_MCP_SCHEMA_URL as Hn, checkPathTraversal as Ht, CODEXCLI_BASH_RULES_FILE_NAME as I, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as In, applyFileMode as It, RulesyncSubagentFrontmatterSchema as J, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Jn, findFilesByGlobs as Jt, getLocalSkillDirNames as K, RULESYNC_RELATIVE_DIR_PATH as Kn, fileExists as Kt, CODEXCLI_DIR as L, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Ln, assertDirectoryIfExists as Lt, AUGMENTCODE_DIR as M, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Mn, withWarnOnceScope as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Nn, CLIError as Nt, COMMANDCODE_DIR as O, CURATED_RULES_FEATURE_SUBDIR as On, fallbackLogger as Ot, caseFoldIdentity as P, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Pn, ErrorCodes as Pt, RulesyncRuleFrontmatterSchema as Q, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Qn, isFileSystemError as Qt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as R, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Rn, assertNoSymlinkBelowRoot as Rt, QWENCODE_DIR as S, ALL_TOOL_TARGETS_WITH_WILDCARD as Sn, assertTargetsFeaturesExclusive as St, LETTACODE_SETTINGS_LOCAL_FILE_NAME as T, ALL_FEATURES as Tn, ConsoleLogger as Tt, CLAUDECODE_SKILLS_DIR_PATH as U, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Un, createTempDirectory as Ut, CLAUDECODE_MEMORIES_DIR_NAME as V, RULESYNC_MCP_RELATIVE_FILE_PATH as Vn, assertWritablePathInsideRoot as Vt, findProjectInstructionBudgetFindings as W, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Wn, directoryExists as Wt, RulesyncSkillFrontmatterSchema as X, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Xn, getHomeDirectory as Xt, RulesyncSkill as Y, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Yn, getFileSizeAndExecutableBit as Yt, RulesyncRule as Z, RULESYNC_USER_CONFIG_DIR_NAME as Zn, isFileNotFoundError as Zt, SubagentsProcessor as _, toPosixPath as _n, resolveEffectiveInputRoots as _t, convertFromTool as a, readFileContent as an, stripControlCharactersKeepingLineFeeds as ar, parseJsonc as at, CommandsProcessor as b, mapWithConcurrency as bn, GITIGNORE_DESTINATION_KEY as bt, parseCommaSeparatedList as c, removeDirectoryStrict as cn, RulesyncCheck as ct, shortenToWidth as d, removeFileWithoutFollowingSymlinks as dn, loadYaml as dt, listDirectoryEntryNames as en, truncateText as er, RulesyncMcp as et, McpProcessor as f, removeTempDirectory as fn, isRecord as ft, RulesProcessor as g, splitPathSegments as gn, mergeInputRootConfigs as gt, ChecksProcessor as h, runWithDirectoryRollback as hn, ConfigResolver as ht, inspectInputRoots as i, posixRelativePathEscapesRoot as in, stripControlCharacters as ir, resolveRulesyncSourceWritePath as it, CODEBUDDY_LOCAL_RULE_FILE_NAME as j, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as jn, resetRunWarningState as jt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as k, FEATURE_SOURCE_TREE_ENTRIES as kn, warnOnConflictingFlags as kt, ELLIPSIS_WIDTH as l, removeFile as ln, RulesyncCheckFrontmatterSchema as lt, HooksProcessor as m, resolvedRelativePath as mn, SKILL_FILE_NAME as mt, formatSourceLoadFailure as n, listSubdirectoryNames as nn, hasEnclosingMarkOutsideKeycap as nr, RulesyncHooks as nt, isPackagingToolTarget as o, readFileContentOrNull as on, stripControlCharactersKeepingLineFeedsAndTabs as or, RulesyncCommand as ot, IgnoreProcessor as p, resolvePath as pn, SHARED_USER_MANAGED_CONFIG_PATHS as pt, RulesyncSubagent as q, RULESYNC_RULES_RELATIVE_DIR_PATH as qn, fileExistsStrict as qt, generate as r, pathEscapesRoot as rn, quoteForLog as rr, getRulesyncSourceCandidates as rt, getProcessorRegistryEntry as s, removeDirectory as sn, stripHiddenCharacters as sr, RulesyncCommandFrontmatterSchema as st, importFromTool as t, listFilePathsRecursively as tn, hasDeceptiveHiddenCharacters as tr, RulesyncIgnore as tt, displayWidthOf as u, removeFileStrict as un, stringifyFrontmatter as ut, SkillsProcessor as v, writeFileBuffer as vn, CONFLICTING_TARGET_PAIRS as vt, LETTACODE_DIR as w, ToolTargetSchema as wn, findControlCharacter as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, ALL_TOOL_TARGETS as xn, SourceEntrySchema as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, writeFileContent as yn, ConfigFileSchema as yt, CLAUDECODE_DIR as z, RULESYNC_IGNORE_RELATIVE_FILE_PATH as zn, assertPathResolvesInsideRoot as zt };
107354
108269
 
107355
- //# sourceMappingURL=import-BWOC-Tig.js.map
108270
+ //# sourceMappingURL=import--Y_A56qx.js.map