rulesync 16.33.0 → 16.34.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.
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
444
444
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
445
445
  //#endregion
446
446
  //#region src/constants/rulesync-paths.ts
447
- const { join: join$342 } = node_path.posix;
447
+ const { join: join$350 } = node_path.posix;
448
448
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
449
449
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
450
450
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
451
451
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
452
452
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
453
453
  const RULES_FEATURE_SUBDIR = "rules";
454
- const CURATED_RULES_FEATURE_SUBDIR = join$342(RULES_FEATURE_SUBDIR, ".curated");
454
+ const CURATED_RULES_FEATURE_SUBDIR = join$350(RULES_FEATURE_SUBDIR, ".curated");
455
455
  const COMMANDS_FEATURE_SUBDIR = "commands";
456
456
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
457
457
  const CHECKS_FEATURE_SUBDIR = "checks";
458
458
  const SKILLS_FEATURE_SUBDIR = "skills";
459
- const CURATED_SKILLS_FEATURE_SUBDIR = join$342(SKILLS_FEATURE_SUBDIR, ".curated");
460
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
- join$342(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
459
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$350(SKILLS_FEATURE_SUBDIR, ".curated");
460
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
+ join$350(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
471
471
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
472
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
472
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
473
473
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
474
474
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
475
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
475
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
477
477
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
478
478
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
479
479
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -503,6 +503,7 @@ const rulesProcessorToolTargetTuple = [
503
503
  "cline",
504
504
  "codebuddy",
505
505
  "codexcli",
506
+ "commandcode",
506
507
  "continue",
507
508
  "copilot",
508
509
  "copilotcli",
@@ -579,6 +580,7 @@ const mcpProcessorToolTargetTuple = [
579
580
  "claudecode-legacy",
580
581
  "cline",
581
582
  "codexcli",
583
+ "commandcode",
582
584
  "continue",
583
585
  "copilot",
584
586
  "copilotcli",
@@ -622,6 +624,7 @@ const commandsProcessorToolTargetTuple = [
622
624
  "claudecode-legacy",
623
625
  "cline",
624
626
  "codexcli",
627
+ "commandcode",
625
628
  "continue",
626
629
  "copilot",
627
630
  "cursor",
@@ -659,6 +662,7 @@ const subagentsProcessorToolTargetTuple = [
659
662
  "claudecode-legacy",
660
663
  "cline",
661
664
  "codexcli",
665
+ "commandcode",
662
666
  "copilot",
663
667
  "copilotcli",
664
668
  "cortexcode",
@@ -700,6 +704,7 @@ const skillsProcessorToolTargetTuple = [
700
704
  "claudecode-legacy",
701
705
  "cline",
702
706
  "codexcli",
707
+ "commandcode",
703
708
  "continue",
704
709
  "copilot",
705
710
  "copilotcli",
@@ -747,6 +752,7 @@ const hooksProcessorToolTargetTuple = [
747
752
  "claudecode",
748
753
  "claudecode-plugin",
749
754
  "codexcli",
755
+ "commandcode",
750
756
  "continue",
751
757
  "copilot",
752
758
  "copilotcli",
@@ -781,6 +787,7 @@ const permissionsProcessorToolTargetTuple = [
781
787
  "claudecode",
782
788
  "cline",
783
789
  "codexcli",
790
+ "commandcode",
784
791
  "continue",
785
792
  "copilot",
786
793
  "copilotcli",
@@ -3295,6 +3302,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3295
3302
  ".claude/settings.json",
3296
3303
  ".claude/settings.local.json",
3297
3304
  ".codex/config.toml",
3305
+ ".commandcode/settings.json",
3298
3306
  ".continue/settings.json",
3299
3307
  ".copilot/settings.json",
3300
3308
  ".github/copilot/settings.json",
@@ -4952,6 +4960,30 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
4952
4960
  };
4953
4961
  const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
4954
4962
  /**
4963
+ * Hook events supported by Command Code.
4964
+ *
4965
+ * Command Code reads hooks from the `hooks` key of `.commandcode/settings.json`
4966
+ * (project) and `~/.commandcode/settings.json` (user) in the Claude-Code shape.
4967
+ * Four events are documented: PreToolUse, PostToolUse, Stop and SessionStart.
4968
+ * Only `command` hooks exist; `timeout` is in seconds (default 30, max 600)
4969
+ * and `$COMMANDCODE_PROJECT_DIR` resolves to the project root.
4970
+ *
4971
+ * @see https://commandcode.ai/docs/hooks
4972
+ */
4973
+ const COMMANDCODE_HOOK_EVENTS = [
4974
+ "preToolUse",
4975
+ "postToolUse",
4976
+ "stop",
4977
+ "sessionStart"
4978
+ ];
4979
+ const CANONICAL_TO_COMMANDCODE_EVENT_NAMES = {
4980
+ preToolUse: "PreToolUse",
4981
+ postToolUse: "PostToolUse",
4982
+ stop: "Stop",
4983
+ sessionStart: "SessionStart"
4984
+ };
4985
+ const COMMANDCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_COMMANDCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
4986
+ /**
4955
4987
  * Hook events supported by the Continue CLI (`cn`).
4956
4988
  *
4957
4989
  * Continue reads a Claude-Code-compatible `hooks` key from
@@ -5160,6 +5192,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
5160
5192
  crush: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5161
5193
  bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5162
5194
  cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5195
+ commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5163
5196
  continue: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5164
5197
  tabnine: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5165
5198
  qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -6179,6 +6212,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
6179
6212
  claudecode: zod_mini.z.optional(toolScopedMcpSchema),
6180
6213
  cline: zod_mini.z.optional(toolScopedMcpSchema),
6181
6214
  codexcli: zod_mini.z.optional(toolScopedMcpSchema),
6215
+ commandcode: zod_mini.z.optional(toolScopedMcpSchema),
6182
6216
  continue: zod_mini.z.optional(toolScopedMcpSchema),
6183
6217
  copilot: zod_mini.z.optional(toolScopedMcpSchema),
6184
6218
  copilotcli: zod_mini.z.optional(toolScopedMcpSchema),
@@ -6574,12 +6608,16 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
6574
6608
  * Targets that share one output file resolve identically so the shared
6575
6609
  * file's content never depends on which of them generates last — see
6576
6610
  * `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
6577
- * and the Antigravity pair).
6611
+ * the Antigravity pair, and the project-scope Claude Code / Command Code
6612
+ * pair, which is why the scope is part of the resolution).
6578
6613
  *
6579
6614
  * Returns the same instance when neither mechanism is used.
6580
6615
  */
6581
- forTarget({ toolTarget, logger }) {
6582
- const { blockKeys, acceptedTargetNames } = resolveMcpTarget({ toolTarget });
6616
+ forTarget({ toolTarget, global = false, logger }) {
6617
+ const { blockKeys, acceptedTargetNames } = resolveMcpTarget({
6618
+ toolTarget,
6619
+ global
6620
+ });
6583
6621
  const json = this.json;
6584
6622
  const sharedServers = this.json.mcpServers ?? {};
6585
6623
  const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
@@ -6666,9 +6704,24 @@ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
6666
6704
  * `config`) — so both targets always apply both blocks in a fixed order
6667
6705
  * (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
6668
6706
  * per server on conflict).
6669
- */
6670
- function resolveMcpTarget({ toolTarget }) {
6671
- if (toolTarget === "claudecode" || toolTarget === "claudecode-legacy") return {
6707
+ * - `claudecode` (and its legacy alias) and `commandcode` share the root
6708
+ * `.mcp.json` in PROJECT mode only (their global files differ:
6709
+ * `~/.claude.json` vs `~/.commandcode/mcp.json`), so in project mode the
6710
+ * three apply the `claudecode` and `commandcode` blocks in that fixed order
6711
+ * (the `commandcode` block wins per server on conflict); in global mode
6712
+ * each reads its own block.
6713
+ */
6714
+ function resolveMcpTarget({ toolTarget, global }) {
6715
+ const isClaudecode = toolTarget === "claudecode" || toolTarget === "claudecode-legacy";
6716
+ if (!global && (isClaudecode || toolTarget === "commandcode")) return {
6717
+ blockKeys: ["claudecode", "commandcode"],
6718
+ acceptedTargetNames: /* @__PURE__ */ new Set([
6719
+ "claudecode",
6720
+ "claudecode-legacy",
6721
+ "commandcode"
6722
+ ])
6723
+ };
6724
+ if (isClaudecode) return {
6672
6725
  blockKeys: ["claudecode"],
6673
6726
  acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
6674
6727
  };
@@ -7833,6 +7886,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
7833
7886
  "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7834
7887
  continue: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7835
7888
  copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7889
+ commandcode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7836
7890
  copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7837
7891
  crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7838
7892
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
@@ -9555,6 +9609,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
9555
9609
  "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
9556
9610
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
9557
9611
  })),
9612
+ commandcode: zod_mini.z.optional(zod_mini.z.looseObject({})),
9558
9613
  cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({})),
9559
9614
  continue: zod_mini.z.optional(zod_mini.z.looseObject({})),
9560
9615
  tabnine: zod_mini.z.optional(zod_mini.z.looseObject({})),
@@ -13166,6 +13221,20 @@ const SHARED_CONFIG_OWNERSHIP = {
13166
13221
  ownedKeys: ["hooks"]
13167
13222
  } }
13168
13223
  },
13224
+ ".commandcode/settings.json": {
13225
+ format: "json",
13226
+ invalidRootPolicy: "error",
13227
+ features: {
13228
+ hooks: {
13229
+ kind: "replace-owned-keys",
13230
+ ownedKeys: ["hooks"]
13231
+ },
13232
+ permissions: {
13233
+ kind: "replace-owned-keys",
13234
+ ownedKeys: ["permissions"]
13235
+ }
13236
+ }
13237
+ },
13169
13238
  ".cortex/settings.json": {
13170
13239
  format: "json",
13171
13240
  invalidRootPolicy: "error",
@@ -15367,6 +15436,94 @@ var CodexcliCommand = class CodexcliCommand extends ToolCommand {
15367
15436
  }
15368
15437
  };
15369
15438
  //#endregion
15439
+ //#region src/constants/commandcode-paths.ts
15440
+ const COMMANDCODE_DIR = ".commandcode";
15441
+ const COMMANDCODE_RULE_FILE_NAME = "AGENTS.md";
15442
+ const COMMANDCODE_SETTINGS_FILE_NAME = "settings.json";
15443
+ const COMMANDCODE_PROJECT_MCP_FILE_NAME = ".mcp.json";
15444
+ const COMMANDCODE_GLOBAL_MCP_FILE_NAME = "mcp.json";
15445
+ const COMMANDCODE_COMMANDS_DIR_PATH = (0, node_path.join)(COMMANDCODE_DIR, "commands");
15446
+ const COMMANDCODE_AGENTS_DIR_PATH = (0, node_path.join)(COMMANDCODE_DIR, "agents");
15447
+ const COMMANDCODE_SKILLS_DIR_PATH = (0, node_path.join)(COMMANDCODE_DIR, "skills");
15448
+ //#endregion
15449
+ //#region src/features/commands/commandcode-command.ts
15450
+ /**
15451
+ * Custom slash command for Command Code.
15452
+ *
15453
+ * Command Code reads Markdown files from `.commandcode/commands/` (project)
15454
+ * and `~/.commandcode/commands/` (user), naming each command after the file's
15455
+ * basename; subdirectories only group the files. The file's full trimmed
15456
+ * body is the prompt that runs — YAML frontmatter is not stripped, only
15457
+ * skipped when the slash menu picks a summary line — so rulesync writes the
15458
+ * bare body. On import a hand-written file's frontmatter block is dropped
15459
+ * (Command Code would send it as prompt text; a rulesync command carries its
15460
+ * own frontmatter) and only the body is kept.
15461
+ *
15462
+ * @see https://commandcode.ai/docs/custom-slash-commands
15463
+ */
15464
+ var CommandcodeCommand = class CommandcodeCommand extends ToolCommand {
15465
+ static getSettablePaths(_options = {}) {
15466
+ return { relativeDirPath: COMMANDCODE_COMMANDS_DIR_PATH };
15467
+ }
15468
+ toRulesyncCommand() {
15469
+ return new RulesyncCommand({
15470
+ outputRoot: process.cwd(),
15471
+ frontmatter: { targets: ["*"] },
15472
+ body: this.getFileContent(),
15473
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
15474
+ relativeFilePath: this.relativeFilePath,
15475
+ fileContent: this.getFileContent(),
15476
+ validate: true
15477
+ });
15478
+ }
15479
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true }) {
15480
+ const paths = this.getSettablePaths();
15481
+ return new CommandcodeCommand({
15482
+ outputRoot,
15483
+ fileContent: rulesyncCommand.getBody(),
15484
+ relativeDirPath: paths.relativeDirPath,
15485
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
15486
+ validate
15487
+ });
15488
+ }
15489
+ validate() {
15490
+ return {
15491
+ success: true,
15492
+ error: null
15493
+ };
15494
+ }
15495
+ getBody() {
15496
+ return this.getFileContent();
15497
+ }
15498
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
15499
+ return this.isTargetedByRulesyncCommandDefault({
15500
+ rulesyncCommand,
15501
+ toolTarget: "commandcode"
15502
+ });
15503
+ }
15504
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
15505
+ const paths = this.getSettablePaths();
15506
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
15507
+ const { body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
15508
+ return new CommandcodeCommand({
15509
+ outputRoot,
15510
+ relativeDirPath: paths.relativeDirPath,
15511
+ relativeFilePath,
15512
+ fileContent: content.trim(),
15513
+ validate
15514
+ });
15515
+ }
15516
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
15517
+ return new CommandcodeCommand({
15518
+ outputRoot,
15519
+ relativeDirPath,
15520
+ relativeFilePath,
15521
+ fileContent: "",
15522
+ validate: false
15523
+ });
15524
+ }
15525
+ };
15526
+ //#endregion
15370
15527
  //#region src/constants/continue-paths.ts
15371
15528
  const CONTINUE_DIR = ".continue";
15372
15529
  const CONTINUE_ROOT_RULE_FILE_NAME = "AGENTS.md";
@@ -19610,6 +19767,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
19610
19767
  supportsSubdirectory: false
19611
19768
  }
19612
19769
  }],
19770
+ ["commandcode", {
19771
+ class: CommandcodeCommand,
19772
+ meta: {
19773
+ extension: "md",
19774
+ supportsProject: true,
19775
+ supportsGlobal: true,
19776
+ isSimulated: false,
19777
+ supportsSubdirectory: true
19778
+ }
19779
+ }],
19613
19780
  ["continue", {
19614
19781
  class: ContinueCommand,
19615
19782
  meta: {
@@ -22980,6 +23147,132 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
22980
23147
  }
22981
23148
  };
22982
23149
  //#endregion
23150
+ //#region src/features/hooks/commandcode-hooks.ts
23151
+ const COMMANDCODE_CONVERTER_CONFIG = {
23152
+ supportedEvents: COMMANDCODE_HOOK_EVENTS,
23153
+ canonicalToToolEventNames: CANONICAL_TO_COMMANDCODE_EVENT_NAMES,
23154
+ toolToCanonicalEventNames: COMMANDCODE_TO_CANONICAL_EVENT_NAMES,
23155
+ projectDirVar: "$COMMANDCODE_PROJECT_DIR",
23156
+ prefixDotRelativeCommandsOnly: true,
23157
+ noMatcherEvents: /* @__PURE__ */ new Set(["stop", "sessionStart"]),
23158
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"])
23159
+ };
23160
+ /**
23161
+ * Single spelling of the settings codec/policy: fail closed on an unparseable
23162
+ * root rather than replacing the user's Command Code settings with generated
23163
+ * output.
23164
+ */
23165
+ function parseCommandcodeSettings$1(fileContent, filePath) {
23166
+ return parseSharedConfig({
23167
+ format: "json",
23168
+ fileContent,
23169
+ filePath,
23170
+ invalidRootPolicy: "error"
23171
+ });
23172
+ }
23173
+ /**
23174
+ * Command Code hooks.
23175
+ *
23176
+ * Hooks live under the top-level `hooks` key of `<project>/.commandcode/settings.json`
23177
+ * (project scope) and `~/.commandcode/settings.json` (user scope), in the
23178
+ * Claude-Code shape: `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{
23179
+ * "type": "command", "command": "...", "timeout"?: <seconds> }] }] }`. Both
23180
+ * files also hold settings rulesync does not own (`permissions`, `defaultMode`,
23181
+ * ...), so generation merges the `hooks` key into the existing file (see
23182
+ * `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
23183
+ *
23184
+ * @see https://commandcode.ai/docs/hooks
23185
+ * @see https://commandcode.ai/docs/settings
23186
+ */
23187
+ var CommandcodeHooks = class CommandcodeHooks extends ToolHooks {
23188
+ constructor(params) {
23189
+ super({
23190
+ ...params,
23191
+ fileContent: params.fileContent ?? "{}"
23192
+ });
23193
+ }
23194
+ isDeletable() {
23195
+ return false;
23196
+ }
23197
+ static getSettablePaths(_options = {}) {
23198
+ return {
23199
+ relativeDirPath: COMMANDCODE_DIR,
23200
+ relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
23201
+ };
23202
+ }
23203
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
23204
+ const paths = CommandcodeHooks.getSettablePaths({ global });
23205
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
23206
+ return new CommandcodeHooks({
23207
+ outputRoot,
23208
+ relativeDirPath: paths.relativeDirPath,
23209
+ relativeFilePath: paths.relativeFilePath,
23210
+ fileContent,
23211
+ validate
23212
+ });
23213
+ }
23214
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
23215
+ const paths = CommandcodeHooks.getSettablePaths({ global });
23216
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
23217
+ const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
23218
+ const config = rulesyncHooks.getJson();
23219
+ const hooks = canonicalToToolHooks({
23220
+ config,
23221
+ toolOverrideHooks: config.commandcode?.hooks,
23222
+ converterConfig: COMMANDCODE_CONVERTER_CONFIG,
23223
+ logger
23224
+ });
23225
+ const fileContent = applySharedConfigPatch({
23226
+ fileKey: sharedConfigFileKey(paths),
23227
+ feature: "hooks",
23228
+ existingContent,
23229
+ patch: { hooks },
23230
+ filePath,
23231
+ logger
23232
+ });
23233
+ return new CommandcodeHooks({
23234
+ outputRoot,
23235
+ relativeDirPath: paths.relativeDirPath,
23236
+ relativeFilePath: paths.relativeFilePath,
23237
+ fileContent,
23238
+ validate
23239
+ });
23240
+ }
23241
+ toRulesyncHooks({ logger } = {}) {
23242
+ const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
23243
+ let settings;
23244
+ try {
23245
+ settings = parseCommandcodeSettings$1(this.getFileContent(), configPath);
23246
+ } catch (error) {
23247
+ throw new Error(`Failed to parse Command Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
23248
+ }
23249
+ const hooks = toolHooksToCanonical({
23250
+ logger,
23251
+ hooks: settings.hooks,
23252
+ converterConfig: COMMANDCODE_CONVERTER_CONFIG
23253
+ });
23254
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
23255
+ hooks,
23256
+ overrideKey: "commandcode"
23257
+ }), null, 2) });
23258
+ }
23259
+ validate() {
23260
+ return {
23261
+ success: true,
23262
+ error: null
23263
+ };
23264
+ }
23265
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
23266
+ return new CommandcodeHooks({
23267
+ outputRoot,
23268
+ relativeDirPath,
23269
+ relativeFilePath,
23270
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
23271
+ validate: false
23272
+ });
23273
+ }
23274
+ };
23275
+ //#endregion
22983
23276
  //#region src/features/hooks/continue-hooks.ts
22984
23277
  const CONTINUE_SPEC = {
22985
23278
  displayName: "Continue",
@@ -28099,6 +28392,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28099
28392
  supportedHookTypes: ["command"],
28100
28393
  supportsMatcher: true
28101
28394
  }],
28395
+ ["commandcode", {
28396
+ class: CommandcodeHooks,
28397
+ meta: {
28398
+ supportsProject: true,
28399
+ supportsGlobal: true,
28400
+ supportsImport: true
28401
+ },
28402
+ supportedEvents: COMMANDCODE_HOOK_EVENTS,
28403
+ supportedHookTypes: ["command"],
28404
+ supportsMatcher: true,
28405
+ matcherEvents: ["preToolUse", "postToolUse"]
28406
+ }],
28102
28407
  ["copilot", {
28103
28408
  class: CopilotHooks,
28104
28409
  meta: {
@@ -31956,6 +32261,254 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
31956
32261
  }
31957
32262
  };
31958
32263
  //#endregion
32264
+ //#region src/features/mcp/commandcode-mcp.ts
32265
+ /**
32266
+ * Parse a Command Code MCP file. The project file is the `.mcp.json` other
32267
+ * agents (Claude Code among them) share, so a hand-written file is read as
32268
+ * JSONC to tolerate comments and trailing commas; malformed content or a
32269
+ * non-object root (`null`, an array, a scalar) fails closed rather than being
32270
+ * spread into the regenerated file.
32271
+ */
32272
+ function parseCommandcodeMcpConfig({ fileContent, relativePath }) {
32273
+ let parsed;
32274
+ try {
32275
+ parsed = parseJsonc$6(fileContent);
32276
+ } catch (error) {
32277
+ throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: ${formatError(error)}`, { cause: error });
32278
+ }
32279
+ if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: expected a JSON object at the root`);
32280
+ return parsed;
32281
+ }
32282
+ /**
32283
+ * The remote transport Command Code reads a server as. Its loader accepts
32284
+ * `http` (streamable HTTP) and `sse` for a `url` server; a bare `url` defaults
32285
+ * to `http`, and the canonical `streamable-http` spelling is folded into it.
32286
+ * A `ws(s)://` URL or any other stated transport has no Command Code
32287
+ * equivalent, so `undefined` tells the caller to skip the server.
32288
+ * @see https://commandcode.ai/docs/mcp
32289
+ */
32290
+ function asCommandcodeRemoteTransport(stated, url) {
32291
+ if (stated === "sse") return "sse";
32292
+ if (stated === "http" || stated === "streamable-http") return "http";
32293
+ if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
32294
+ }
32295
+ /**
32296
+ * Convert the canonical server map to the shape Command Code's config schema
32297
+ * documents (used for the global `~/.commandcode/mcp.json` only — the project
32298
+ * `.mcp.json` is shared with Claude Code and written pass-through, see the
32299
+ * class doc): a remote server is `{ transport, url, headers?, env?, oauth? }`
32300
+ * and a stdio server is `{ transport: "stdio", command, args?, env? }`, each
32301
+ * with an optional `enabled` flag. The documented spelling is `transport`
32302
+ * (`type` is only accepted as an alias on read), so that is what is written,
32303
+ * and only the documented keys are emitted — `timeout`, rulesync-only fields
32304
+ * and anything else is left out. The canonical `disabled: true` becomes
32305
+ * Command Code's native `enabled: false` (the rulesync-source-only `enabled`
32306
+ * filter never reaches this function: `getMcpServers()` drops such a server).
32307
+ *
32308
+ * A server Command Code drops at load time — no transport at all, a remote
32309
+ * transport without a URL, a WebSocket URL, or a stdio entry without a
32310
+ * command — is skipped with a warning rather than written in a form the tool
32311
+ * would silently ignore.
32312
+ * @see https://commandcode.ai/docs/mcp
32313
+ */
32314
+ function convertToCommandcodeFormat(mcpServers, logger) {
32315
+ const result = {};
32316
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
32317
+ if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
32318
+ if (declaresNoTransport(serverConfig)) {
32319
+ warnAndSkipMcpServer({
32320
+ toolName: "Command Code",
32321
+ serverName,
32322
+ reason: "no transport",
32323
+ logger
32324
+ });
32325
+ continue;
32326
+ }
32327
+ const converted = isRemoteMcpServer(serverConfig) ? convertRemoteServer$1({
32328
+ serverName,
32329
+ serverConfig,
32330
+ logger
32331
+ }) : convertStdioServer$1({
32332
+ serverName,
32333
+ serverConfig,
32334
+ logger
32335
+ });
32336
+ if (converted === void 0) continue;
32337
+ if (serverConfig.disabled === true) converted.enabled = false;
32338
+ if (isRecord$1(serverConfig.env)) converted.env = omitPrototypePollutionKeys(serverConfig.env);
32339
+ result[serverName] = converted;
32340
+ }
32341
+ return result;
32342
+ }
32343
+ function convertRemoteServer$1({ serverName, serverConfig, logger }) {
32344
+ const url = resolveRemoteMcpUrl(serverConfig);
32345
+ if (!url) {
32346
+ warnAndSkipMcpServer({
32347
+ toolName: "Command Code",
32348
+ serverName,
32349
+ reason: "a remote transport without a url",
32350
+ logger
32351
+ });
32352
+ return;
32353
+ }
32354
+ const stated = serverConfig.type ?? serverConfig.transport;
32355
+ const transport = asCommandcodeRemoteTransport(typeof stated === "string" ? stated : void 0, url);
32356
+ if (transport === void 0) {
32357
+ warnAndSkipMcpServer({
32358
+ toolName: "Command Code",
32359
+ serverName,
32360
+ reason: stated === void 0 ? "a WebSocket url, which Command Code's remote transports (http and sse) cannot reach" : `the "${String(stated)}" transport, which Command Code does not offer for remote servers (only http and sse)`,
32361
+ logger
32362
+ });
32363
+ return;
32364
+ }
32365
+ const converted = {
32366
+ transport,
32367
+ url
32368
+ };
32369
+ if (isRecord$1(serverConfig.headers)) converted.headers = omitPrototypePollutionKeys(serverConfig.headers);
32370
+ if (isRecord$1(serverConfig.oauth)) converted.oauth = omitPrototypePollutionKeys(serverConfig.oauth);
32371
+ return converted;
32372
+ }
32373
+ function convertStdioServer$1({ serverName, serverConfig, logger }) {
32374
+ const [command, ...args] = resolveLocalMcpCommand(serverConfig);
32375
+ if (!command) {
32376
+ warnAndSkipMcpServer({
32377
+ toolName: "Command Code",
32378
+ serverName,
32379
+ reason: "a stdio transport without a command",
32380
+ logger
32381
+ });
32382
+ return;
32383
+ }
32384
+ const converted = {
32385
+ transport: "stdio",
32386
+ command
32387
+ };
32388
+ if (args.length > 0) converted.args = args;
32389
+ return converted;
32390
+ }
32391
+ /**
32392
+ * Convert Command Code's server map back to the canonical shape. Both
32393
+ * spellings Command Code accepts (`transport` and its `type` alias, with
32394
+ * `command`/`url`) are already canonical, so entries pass through with only
32395
+ * prototype-pollution keys dropped; the native `enabled: false` maps to the
32396
+ * canonical `disabled: true` (a bare `enabled` would otherwise be read back
32397
+ * as rulesync's own generation filter and silently drop the server from every
32398
+ * other target).
32399
+ */
32400
+ function convertFromCommandcodeFormat(mcpServers) {
32401
+ if (!isMcpServers(mcpServers)) return {};
32402
+ const result = {};
32403
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
32404
+ if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
32405
+ const { enabled, ...rest } = omitPrototypePollutionKeys(serverConfig);
32406
+ result[serverName] = enabled === false ? {
32407
+ ...rest,
32408
+ disabled: true
32409
+ } : rest;
32410
+ }
32411
+ return result;
32412
+ }
32413
+ /**
32414
+ * Command Code MCP configuration.
32415
+ *
32416
+ * Command Code reads `.mcp.json` at the project root (project scope, meant to
32417
+ * be committed) and `~/.commandcode/mcp.json` (user scope), both in the
32418
+ * `{ "mcpServers": { ... } }` shape the docs show; the per-machine local
32419
+ * scope under `~/.commandcode/projects/` is left to the tool. Top-level
32420
+ * sibling keys of an existing file are kept. The global file is not deleted
32421
+ * by `--delete` (it lives outside the project), while the project file is
32422
+ * rulesync's own and is.
32423
+ *
32424
+ * The project `.mcp.json` is the very file the `claudecode` target writes, so
32425
+ * at project scope the servers are written in the same pass-through shape
32426
+ * `ClaudecodeMcp` uses: whichever of the two targets generates last leaves
32427
+ * byte-identical content, and Command Code reads that shape natively (`type`
32428
+ * is an alias of `transport`, a bare `url` infers `http`, and unknown keys
32429
+ * are ignored). Only the global file — Command Code's own — gets the
32430
+ * `transport` / `enabled: false` rewrite of `convertToCommandcodeFormat`.
32431
+ *
32432
+ * @see https://commandcode.ai/docs/mcp
32433
+ */
32434
+ var CommandcodeMcp = class CommandcodeMcp extends ToolMcp {
32435
+ json;
32436
+ constructor(params) {
32437
+ super(params);
32438
+ this.json = this.fileContent === void 0 ? {} : parseCommandcodeMcpConfig({
32439
+ fileContent: this.fileContent,
32440
+ relativePath: (0, node_path.join)(this.relativeDirPath, this.relativeFilePath)
32441
+ });
32442
+ }
32443
+ getJson() {
32444
+ return this.json;
32445
+ }
32446
+ isDeletable() {
32447
+ return !this.global;
32448
+ }
32449
+ static getSettablePaths({ global = false } = {}) {
32450
+ return global ? {
32451
+ relativeDirPath: COMMANDCODE_DIR,
32452
+ relativeFilePath: COMMANDCODE_GLOBAL_MCP_FILE_NAME
32453
+ } : {
32454
+ relativeDirPath: ".",
32455
+ relativeFilePath: COMMANDCODE_PROJECT_MCP_FILE_NAME
32456
+ };
32457
+ }
32458
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
32459
+ const paths = this.getSettablePaths({ global });
32460
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
32461
+ return new CommandcodeMcp({
32462
+ outputRoot,
32463
+ relativeDirPath: paths.relativeDirPath,
32464
+ relativeFilePath: paths.relativeFilePath,
32465
+ fileContent,
32466
+ validate,
32467
+ global
32468
+ });
32469
+ }
32470
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
32471
+ const paths = this.getSettablePaths({ global });
32472
+ const json = parseCommandcodeMcpConfig({
32473
+ fileContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
32474
+ relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
32475
+ });
32476
+ const mcpServers = global ? convertToCommandcodeFormat(rulesyncMcp.getMcpServers(), logger) : rulesyncMcp.getMcpServers();
32477
+ const commandcodeConfig = {
32478
+ ...json,
32479
+ mcpServers
32480
+ };
32481
+ return new CommandcodeMcp({
32482
+ outputRoot,
32483
+ relativeDirPath: paths.relativeDirPath,
32484
+ relativeFilePath: paths.relativeFilePath,
32485
+ fileContent: JSON.stringify(commandcodeConfig, null, 2),
32486
+ validate,
32487
+ global
32488
+ });
32489
+ }
32490
+ toRulesyncMcp() {
32491
+ const mcpServers = convertFromCommandcodeFormat(this.json.mcpServers);
32492
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
32493
+ }
32494
+ validate() {
32495
+ return {
32496
+ success: true,
32497
+ error: null
32498
+ };
32499
+ }
32500
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
32501
+ return new CommandcodeMcp({
32502
+ outputRoot,
32503
+ relativeDirPath,
32504
+ relativeFilePath,
32505
+ fileContent: "{}",
32506
+ validate: false,
32507
+ global
32508
+ });
32509
+ }
32510
+ };
32511
+ //#endregion
31959
32512
  //#region src/features/mcp/continue-mcp.ts
31960
32513
  /**
31961
32514
  * Parse a Continue MCP file. Continue reads the files under `mcpServers/` as
@@ -36924,6 +37477,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
36924
37477
  static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
36925
37478
  const servers = rulesyncMcp.forTarget({
36926
37479
  toolTarget: "rovodev",
37480
+ global,
36927
37481
  logger
36928
37482
  }).getMcpServers();
36929
37483
  const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
@@ -38221,6 +38775,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
38221
38775
  supportsDisabledTools: true
38222
38776
  }
38223
38777
  }],
38778
+ ["commandcode", {
38779
+ class: CommandcodeMcp,
38780
+ meta: {
38781
+ supportsProject: true,
38782
+ supportsGlobal: true,
38783
+ supportsEnabledTools: false,
38784
+ supportsDisabledTools: false
38785
+ }
38786
+ }],
38224
38787
  ["continue", {
38225
38788
  class: ContinueMcp,
38226
38789
  meta: {
@@ -38614,6 +39177,7 @@ var McpProcessor = class extends FeatureProcessor {
38614
39177
  const toolMcps = await Promise.all([rulesyncMcp].map(async (mcp) => {
38615
39178
  const targetedRulesyncMcp = mcp.forTarget({
38616
39179
  toolTarget: this.toolTarget,
39180
+ global: this.global,
38617
39181
  logger: this.logger
38618
39182
  });
38619
39183
  const fieldsToStrip = [];
@@ -42608,6 +43172,622 @@ function mapBashActionToDecision(action) {
42608
43172
  return "forbidden";
42609
43173
  }
42610
43174
  //#endregion
43175
+ //#region src/features/permissions/commandcode-permissions.ts
43176
+ /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
43177
+ const COMMANDCODE_PERMISSIONS_KEY = "permissions";
43178
+ const CATCH_ALL_PATTERN$6 = "*";
43179
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
43180
+ const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
43181
+ const CATEGORY_TO_COMMANDCODE_TOOL = {
43182
+ bash: "Shell",
43183
+ read: "Read",
43184
+ edit: "Edit",
43185
+ write: "Write",
43186
+ grep: "Grep",
43187
+ glob: "Glob",
43188
+ webfetch: "WebFetch",
43189
+ websearch: "WebSearch"
43190
+ };
43191
+ const COMMANDCODE_TOOL_TO_CATEGORY = {
43192
+ ...Object.fromEntries(Object.entries(CATEGORY_TO_COMMANDCODE_TOOL).map(([category, tool]) => [tool.toLowerCase(), category])),
43193
+ bash: "bash",
43194
+ powershell: "bash",
43195
+ shell_command: "bash",
43196
+ monitor_command: "bash",
43197
+ kill_shell: "bash",
43198
+ notebookedit: "edit",
43199
+ write_file: "write",
43200
+ web_fetch: "webfetch",
43201
+ web_search: "websearch"
43202
+ };
43203
+ const ACTION_RANK$1 = {
43204
+ allow: 0,
43205
+ ask: 1,
43206
+ deny: 2
43207
+ };
43208
+ /** Whether `action` outranks `existing` (deny > ask > allow). */
43209
+ function isStricterAction({ action, existing }) {
43210
+ return existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing];
43211
+ }
43212
+ /**
43213
+ * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
43214
+ * the whitespace makes it a pattern rule, and a shell pattern grants only
43215
+ * commands that pass Command Code's pattern gate.
43216
+ */
43217
+ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (the command must parse into words and carry no environment assignment)";
43218
+ /**
43219
+ * Build a Command Code rule (`Shell(git *)`, `Read`, `mcp__github__get_issue`,
43220
+ * `mcp__*`, `*`) from a canonical category + pattern. Returns `null` for
43221
+ * categories Command Code cannot express so the caller can skip them.
43222
+ *
43223
+ * MCP tools are keyed by their canonical `mcp__server__tool` name, so a
43224
+ * scoped `mcp__<remainder>` category is written verbatim and the bare `mcp`
43225
+ * category becomes `mcp__*` (or `mcp__<pattern>` when the pattern names a
43226
+ * server or tool). A pattern on an MCP category is still written as the
43227
+ * `(specifier)` here; `writableCommandcodeRule` decides what to do with it,
43228
+ * because Command Code ignores it. The all-tools `*` category is only
43229
+ * written for its catch-all pattern; narrower `*` patterns are shell
43230
+ * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
43231
+ */
43232
+ function buildCommandcodeRule(category, pattern) {
43233
+ const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
43234
+ if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
43235
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
43236
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
43237
+ const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
43238
+ if (tool === void 0) return null;
43239
+ return catchAll ? tool : `${tool}(${pattern})`;
43240
+ }
43241
+ /**
43242
+ * Split `Tool(specifier)` into its two halves the way Command Code's
43243
+ * `splitRule` does (`command-code` 1.54.0): the rule is split at the first
43244
+ * unescaped `(` and must end with `)`, the tool half is trimmed (so
43245
+ * `Shell (rm -rf *)` is a `Shell` rule), and a `\(` / `\)` in the specifier
43246
+ * is an escaped parenthesis. `Tool` alone has an empty specifier. A rule that
43247
+ * opens a parenthesis without closing one is not a rule to Command Code at
43248
+ * all, so it comes back with an empty tool half and stays unmodeled. Nothing
43249
+ * else in the specifier is touched: Command Code does not trim it (the shell
43250
+ * matcher normalizes whitespace on its own, see `parseCommandcodeRule`), so
43251
+ * `Read( )` is a rule for the pattern `" "`, which matches nothing, not for
43252
+ * the whole tool.
43253
+ */
43254
+ function splitCommandcodeRule(rule) {
43255
+ const trimmed = rule.trim();
43256
+ const parenIndex = trimmed.search(/(?<!\\)\(/);
43257
+ if (parenIndex === -1) return {
43258
+ tool: trimmed,
43259
+ inner: ""
43260
+ };
43261
+ if (!trimmed.endsWith(")")) return {
43262
+ tool: "",
43263
+ inner: ""
43264
+ };
43265
+ return {
43266
+ tool: trimmed.slice(0, parenIndex).trim(),
43267
+ inner: trimmed.slice(parenIndex + 1, -1).replace(/\\([()])/g, "$1")
43268
+ };
43269
+ }
43270
+ /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
43271
+ function isCatchAllSpecifier(inner) {
43272
+ return inner === "" || inner === CATCH_ALL_PATTERN$6;
43273
+ }
43274
+ /**
43275
+ * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
43276
+ * `parsePermissionRule` / `parseMcpToken`). The MCP shape is only recognized
43277
+ * by its exact `mcp__` prefix, and such a rule never carries a specifier:
43278
+ * `mcp__<server>__<tool>(owner:foo)` denies, asks or allows the whole tool.
43279
+ * Its names are matched case-sensitively against the registered server and
43280
+ * tool (Command Code never lowercases them), so the remainder keeps its case
43281
+ * as the canonical category. `mcp__<server>`, `mcp__<server>__` and
43282
+ * `mcp__<server>__*` are the whole server (category `mcp__<server>`), and a
43283
+ * `*` server (`mcp__*`, `mcp__*__<tool>`) is every MCP tool, honored in
43284
+ * `deny`/`ask` only. A differently-cased `MCP__...` spelling is read as a
43285
+ * plain tool-name rule instead, matched case-insensitively:
43286
+ * `MCP__<server>__<tool>` matches that tool by name (so it folds onto the
43287
+ * `mcp__` category, remainder as written), `MCP__<server>__<tool>(specifier)`
43288
+ * globs the specifier against the call's `command` / `file_path` / `path` /
43289
+ * `url` / `pattern` argument (in `deny`/`ask` a `param:glob` form is tried
43290
+ * first) — kept as a pattern in `deny`/`ask`, but not imported from `allow`,
43291
+ * where a grant scoped that way cannot be modeled without widening it; the
43292
+ * globs
43293
+ * `MCP__*` (every MCP tool) and `MCP__<server>__*` (the whole server) match
43294
+ * by name in `deny`/`ask` only, and `MCP__<server>` names no tool at all.
43295
+ * Whatever matches nothing anywhere (or a glob rulesync does not model)
43296
+ * returns `null` and is left alone.
43297
+ */
43298
+ function parseMcpCommandcodeRule({ tool, inner }) {
43299
+ const remainder = tool.slice(5);
43300
+ const separator = remainder.indexOf("__");
43301
+ const server = separator === -1 ? remainder : remainder.slice(0, separator);
43302
+ const rest = separator === -1 ? "" : remainder.slice(separator + 2);
43303
+ if (server.length === 0) return null;
43304
+ const everyServer = {
43305
+ category: "mcp",
43306
+ pattern: CATCH_ALL_PATTERN$6,
43307
+ notInAllow: true
43308
+ };
43309
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
43310
+ if (server === CATCH_ALL_PATTERN$6) return everyServer;
43311
+ if (server.includes(CATCH_ALL_PATTERN$6)) return null;
43312
+ return {
43313
+ category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
43314
+ pattern: CATCH_ALL_PATTERN$6,
43315
+ notInAllow: false
43316
+ };
43317
+ }
43318
+ const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
43319
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
43320
+ if (!isCatchAllSpecifier(inner)) return namesTool ? {
43321
+ category,
43322
+ pattern: inner,
43323
+ notInAllow: true
43324
+ } : null;
43325
+ if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
43326
+ if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
43327
+ category: `${MCP_CANONICAL_PREFIX$5}${server}`,
43328
+ pattern: CATCH_ALL_PATTERN$6,
43329
+ notInAllow: true
43330
+ };
43331
+ return namesTool ? {
43332
+ category,
43333
+ pattern: CATCH_ALL_PATTERN$6,
43334
+ notInAllow: false
43335
+ } : null;
43336
+ }
43337
+ /**
43338
+ * A rule on one of the friendly tools, with its specifier read as Command
43339
+ * Code matches it. The shell matcher trims and collapses whitespace on both
43340
+ * the pattern and the command, so `Shell( git * )` is `Shell(git *)` and a
43341
+ * pattern that is blank once normalized (`Shell( )`), or a bare `:*` whose
43342
+ * prefix is empty (`Shell(:*)`), matches nothing. A padded `Shell( * )` is
43343
+ * not the bare `Shell`: only a specifier of exactly
43344
+ * `` or `*` is dropped by the rule parser, so `( * )` stays a pattern rule —
43345
+ * in `deny`/`ask` it matches every command (the whole tool), while in `allow`
43346
+ * it is narrower than the bare `Shell` (it needs a command that parses into
43347
+ * words and has no environment-assignment prefix), so it is the whole tool
43348
+ * marked `notInAllow`. The path, web and tool-name matchers use the
43349
+ * specifier as written, so one that is blank or `*` only once trimmed
43350
+ * (`Read( * )`) matches nothing. What matches nothing comes back as `null`:
43351
+ * the rule is unmodeled and left alone.
43352
+ */
43353
+ function friendlyToolRule({ category, inner }) {
43354
+ if (category === "bash") {
43355
+ if (isCatchAllSpecifier(inner)) return {
43356
+ category,
43357
+ pattern: CATCH_ALL_PATTERN$6,
43358
+ notInAllow: false
43359
+ };
43360
+ const normalized = inner.trim().replace(/\s+/g, " ");
43361
+ if (normalized === "" || normalized === ":*") return null;
43362
+ return isCatchAllSpecifier(normalized) ? {
43363
+ category,
43364
+ pattern: CATCH_ALL_PATTERN$6,
43365
+ notInAllow: true
43366
+ } : {
43367
+ category,
43368
+ pattern: normalized,
43369
+ notInAllow: false
43370
+ };
43371
+ }
43372
+ if (isCatchAllSpecifier(inner)) return {
43373
+ category,
43374
+ pattern: CATCH_ALL_PATTERN$6,
43375
+ notInAllow: false
43376
+ };
43377
+ return isCatchAllSpecifier(inner.trim()) ? null : {
43378
+ category,
43379
+ pattern: inner,
43380
+ notInAllow: false
43381
+ };
43382
+ }
43383
+ /**
43384
+ * Parse a Command Code rule back into canonical terms. Tool names fold case;
43385
+ * `Tool`, `Tool()` and `Tool(*)` all mean the whole tool; MCP-shaped rules
43386
+ * go through `parseMcpCommandcodeRule`. Returns `null` for a rule rulesync
43387
+ * cannot model (an exact internal tool name such as `edit_file`, a
43388
+ * name-wildcard like `edit_*`, a specifier on a rule that takes none, or a
43389
+ * rule Command Code enforces nothing for, such as `Agent`).
43390
+ */
43391
+ function parseCommandcodeRule(rule) {
43392
+ const { tool, inner } = splitCommandcodeRule(rule);
43393
+ if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
43394
+ category: "*",
43395
+ pattern: CATCH_ALL_PATTERN$6,
43396
+ notInAllow: true
43397
+ } : null;
43398
+ const lowered = tool.toLowerCase();
43399
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
43400
+ tool,
43401
+ inner
43402
+ });
43403
+ const category = Object.hasOwn(COMMANDCODE_TOOL_TO_CATEGORY, lowered) ? COMMANDCODE_TOOL_TO_CATEGORY[lowered] : void 0;
43404
+ if (category === void 0) return null;
43405
+ return friendlyToolRule({
43406
+ category,
43407
+ inner
43408
+ });
43409
+ }
43410
+ /**
43411
+ * Whether an imported rule is a differently-cased `MCP__<server>__<tool>`
43412
+ * name that folded onto an exact-prefix `mcp__` category (which Command Code
43413
+ * matches case-sensitively, unlike the name rule it came from).
43414
+ */
43415
+ function isFoldedMcpToolNameRule({ rule, category }) {
43416
+ const { tool } = splitCommandcodeRule(rule);
43417
+ return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
43418
+ }
43419
+ /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
43420
+ function scopedMcpRuleTool(rule) {
43421
+ const { tool, inner } = splitCommandcodeRule(rule);
43422
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
43423
+ }
43424
+ /**
43425
+ * The string entries of a Command Code permission list. Command Code reads
43426
+ * each entry on its own and skips one that is not a string, so a stray
43427
+ * `null` beside `Shell(rm -rf *)` must not throw the deny away with it; the
43428
+ * skipped entries are counted in a warning because they cannot be kept.
43429
+ */
43430
+ function stringRules({ list, action, outcome, warn }) {
43431
+ if (!Array.isArray(list)) return [];
43432
+ const rules = list.filter((entry) => typeof entry === "string");
43433
+ const skipped = list.length - rules.length;
43434
+ if (skipped > 0) warn(`Command Code permission list "${action}" holds ${skipped} ${skipped === 1 ? "entry" : "entries"} that ${skipped === 1 ? "is" : "are"} not a string, which Command Code skips; ${skipped === 1 ? "that entry was" : "those entries were"} ${outcome}.`);
43435
+ return rules;
43436
+ }
43437
+ /**
43438
+ * The canonical categories this run rebuilds — every category the canonical
43439
+ * config names (an empty `bash: {}` still reclaims the previous `Shell(...)`
43440
+ * entries, as in the Claude Code adapter) plus every rule it emits, keyed the
43441
+ * way an existing entry parses back (so `mcp: { github: "deny" }`, written as
43442
+ * `mcp__github`, claims the `mcp__github` entries of the previous run; case
43443
+ * variants of the friendly tool names fold together, while MCP names are
43444
+ * matched exactly, as Command Code does). An existing entry whose tool folds onto one of them is
43445
+ * rulesync's to replace, whatever list it sits in — otherwise flipping a rule
43446
+ * from deny to allow would leave the old deny behind and win. Every other
43447
+ * entry — an internal tool name rulesync cannot model, or a modeled tool the
43448
+ * canonical config does not mention — is the user's (Command Code writes
43449
+ * interactive approvals into the same lists) and is preserved verbatim.
43450
+ * A `deny`/`ask` entry that is reclaimed without an equally strict rule
43451
+ * taking its place (say a hand-written `mcp__*` deny next to a canonical
43452
+ * `mcp: { github: "deny" }`) is reported, so regenerating never silently
43453
+ * loosens what the user wrote. Mirrors `managedClaudeToolNames` in the
43454
+ * Claude Code adapter.
43455
+ */
43456
+ function preservedRules({ existingPermissions, key, managedCategories, written, logger }) {
43457
+ return stringRules({
43458
+ list: existingPermissions[key],
43459
+ action: key,
43460
+ outcome: "dropped from the regenerated list",
43461
+ warn: (message) => logger?.warn(message)
43462
+ }).filter((rule) => {
43463
+ const parsed = parseCommandcodeRule(rule);
43464
+ if (parsed === null || !managedCategories.has(parsed.category)) return true;
43465
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
43466
+ if (key !== "allow" && isStricterAction({
43467
+ action: key,
43468
+ existing: replacement
43469
+ })) logger?.warn(`Command Code permission rule '${rule}' in "${key}" belongs to the '${parsed.category}' category, which the rulesync config now manages, and was replaced by its rules.`);
43470
+ return false;
43471
+ });
43472
+ }
43473
+ /**
43474
+ * The rule to write for a canonical rule, or `null` when Command Code would
43475
+ * ignore it in the `action` list. A server-less wildcard (`*`, `mcp__*`) in
43476
+ * `allow` is warned about and left unwritten rather than written dead. A
43477
+ * specifier on an MCP rule is ignored by Command Code in every list, so a
43478
+ * scoped `deny`/`ask` is written as the bare tool (which is what Command
43479
+ * Code would enforce anyway, only now visibly) with a warning, and a scoped
43480
+ * `allow` — which would grant the whole tool — is refused. Judged on the
43481
+ * rule as written, so the bare `mcp` category cannot smuggle a specifier in
43482
+ * through a `<server>__<tool>(specifier)` pattern.
43483
+ */
43484
+ function writableCommandcodeRule({ rule, emitted, action, logger }) {
43485
+ if (action === "allow" && emitted.notInAllow) {
43486
+ logger?.warn(emitted.category === "bash" ? `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, so the '${emitted.category}' allow rule was not written; use the '*' pattern for the whole tool.` : `Command Code does not honor '${rule}' in "allow" as written (an allow rule must name what it grants), so the '${emitted.category}' allow rule was not written.`);
43487
+ return null;
43488
+ }
43489
+ const tool = scopedMcpRuleTool(rule);
43490
+ if (tool === null) return rule;
43491
+ if (action === "allow") {
43492
+ logger?.warn(`Command Code ignores the specifier of an MCP rule and would allow the whole '${tool}' tool, so the '${rule}' allow rule was not written.`);
43493
+ return null;
43494
+ }
43495
+ logger?.warn(`Command Code ignores the specifier of an MCP rule, so '${rule}' was written as '${tool}' and applies "${action}" to the whole tool.`);
43496
+ return tool;
43497
+ }
43498
+ /**
43499
+ * Record `action` for `rule`, keeping the stricter one (deny > ask > allow)
43500
+ * when two canonical rules collapse onto the same Command Code entry.
43501
+ */
43502
+ function rankCommandcodeRule({ ranked, rule, action, logger }) {
43503
+ const existing = ranked.get(rule);
43504
+ if (existing !== void 0 && existing !== action) logger?.warn(`Command Code permission rule '${rule}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
43505
+ if (isStricterAction({
43506
+ action,
43507
+ existing
43508
+ })) ranked.set(rule, action);
43509
+ }
43510
+ /**
43511
+ * Bucket the canonical rules into Command Code's `allow`/`ask`/`deny` lists.
43512
+ * Collisions resolve to the strictest action (deny > ask > allow); categories
43513
+ * Command Code cannot express are skipped with a warning when they carry a
43514
+ * `deny`; rules Command Code would ignore in the target list are dropped or
43515
+ * rewritten by `writableCommandcodeRule`.
43516
+ */
43517
+ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
43518
+ const permission = honorAllToolsOnBash(config.permission);
43519
+ const ranked = /* @__PURE__ */ new Map();
43520
+ const managedCategories = /* @__PURE__ */ new Set();
43521
+ for (const [category, rules] of Object.entries(permission)) {
43522
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
43523
+ const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
43524
+ if (managedCategory !== null) managedCategories.add(managedCategory.category);
43525
+ for (const [pattern, action] of Object.entries(rules)) {
43526
+ const rule = buildCommandcodeRule(category, pattern);
43527
+ const emitted = rule === null ? null : parseCommandcodeRule(rule);
43528
+ if (rule === null || emitted === null) {
43529
+ const honoredOnShell = category === "*" && permission.bash !== void 0;
43530
+ if (action === "deny" && !honoredOnShell) logger?.warn(`Command Code has no permission rule for the '${category}' category with pattern '${pattern}'; its 'deny' rule could not be represented and was skipped.`);
43531
+ continue;
43532
+ }
43533
+ managedCategories.add(emitted.category);
43534
+ const written = writableCommandcodeRule({
43535
+ rule,
43536
+ emitted,
43537
+ action,
43538
+ logger
43539
+ });
43540
+ if (written !== null) rankCommandcodeRule({
43541
+ ranked,
43542
+ rule: written,
43543
+ action,
43544
+ logger
43545
+ });
43546
+ }
43547
+ }
43548
+ const written = /* @__PURE__ */ new Map();
43549
+ for (const [rule, action] of ranked) {
43550
+ const parsed = parseCommandcodeRule(rule);
43551
+ if (parsed !== null) written.set(`${parsed.category}(${parsed.pattern})`, action);
43552
+ }
43553
+ const preserved = {
43554
+ existingPermissions,
43555
+ managedCategories,
43556
+ written,
43557
+ logger
43558
+ };
43559
+ const allow = preservedRules({
43560
+ ...preserved,
43561
+ key: "allow"
43562
+ });
43563
+ const ask = preservedRules({
43564
+ ...preserved,
43565
+ key: "ask"
43566
+ });
43567
+ const deny = preservedRules({
43568
+ ...preserved,
43569
+ key: "deny"
43570
+ });
43571
+ for (const [rule, action] of ranked) if (action === "allow") allow.push(rule);
43572
+ else if (action === "ask") ask.push(rule);
43573
+ else deny.push(rule);
43574
+ return {
43575
+ allow: (0, es_toolkit.uniq)(allow.toSorted()),
43576
+ ask: (0, es_toolkit.uniq)(ask.toSorted()),
43577
+ deny: (0, es_toolkit.uniq)(deny.toSorted())
43578
+ };
43579
+ }
43580
+ /**
43581
+ * Why a rule marked `notInAllow` is not imported from `allow`: Command Code
43582
+ * ignores a server-less wildcard or a tool-name glob there, while it does
43583
+ * enforce a scoped `MCP__<server>__<tool>(specifier)` — as a glob over the
43584
+ * call's arguments — and a padded `Shell( * )` — as a pattern narrower than
43585
+ * the bare `Shell` — which rulesync cannot model in `allow` without widening
43586
+ * the grant.
43587
+ */
43588
+ function skippedAllowImportMessage({ rule, category }) {
43589
+ const { tool, inner } = splitCommandcodeRule(rule);
43590
+ 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.`;
43591
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$5) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
43592
+ 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.`;
43593
+ }
43594
+ /**
43595
+ * Parse Command Code's `permissions` lists back into a canonical permission
43596
+ * map with `deny > ask > allow` precedence, so a tool described more than
43597
+ * once resolves to the strictest action. Each rule is imported as what
43598
+ * Command Code enforces for it, never as what it looks like: a rule Command
43599
+ * Code ignores in `allow` (`*`, `mcp__*`, an `MCP__...` glob) is skipped
43600
+ * there with a warning, since importing it would turn a dead line into a
43601
+ * live grant for every other target, and so is a scoped
43602
+ * `MCP__<server>__<tool>(specifier)` allow, which Command Code enforces as a
43603
+ * glob over the call's arguments but rulesync cannot import without widening
43604
+ * it to the whole tool; the specifier of an exact-prefix `mcp__...` rule,
43605
+ * which Command Code drops, is folded to the whole tool in every list — with
43606
+ * a warning in `allow`, where the fold widens the grant the user wrote. A
43607
+ * tool-half glob in that exact-prefix spelling (`mcp__<server>__get_*`), which
43608
+ * Command Code matches against tool names in every list, is imported as the
43609
+ * category spelled that way, the same one the generator writes back; other
43610
+ * targets read it as a literal tool name.
43611
+ */
43612
+ function parseCommandcodeRuleLists(permissions) {
43613
+ const permission = {};
43614
+ const lists = [
43615
+ ["allow", permissions.allow],
43616
+ ["ask", permissions.ask],
43617
+ ["deny", permissions.deny]
43618
+ ];
43619
+ for (const [action, rawList] of lists) {
43620
+ const list = stringRules({
43621
+ list: rawList,
43622
+ action,
43623
+ outcome: "not imported",
43624
+ warn: (message) => fallbackLogger.warn(message)
43625
+ });
43626
+ for (const rule of list) {
43627
+ const parsed = parseCommandcodeRule(rule);
43628
+ if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) {
43629
+ if (action !== "allow") fallbackLogger.warn(`Command Code permission rule '${rule}' in "${action}" is not one rulesync can model, so it could not be imported; it stays in the Command Code settings, where a regenerate keeps it unless the canonical config manages its category.`);
43630
+ continue;
43631
+ }
43632
+ const { category, pattern } = parsed;
43633
+ if (action === "allow" && parsed.notInAllow) {
43634
+ fallbackLogger.warn(skippedAllowImportMessage({
43635
+ rule,
43636
+ category
43637
+ }));
43638
+ continue;
43639
+ }
43640
+ if (action === "allow" && scopedMcpRuleTool(rule) !== null) fallbackLogger.warn(`Command Code ignores the specifier of an MCP rule, so '${rule}' in "allow" was imported as the whole '${category}' tool, which is what Command Code grants for it.`);
43641
+ if (isFoldedMcpToolNameRule({
43642
+ rule,
43643
+ category
43644
+ })) fallbackLogger.warn(`Command Code matches '${rule}' in "${action}" against MCP tool names case-insensitively, but the '${category}' rule generated from it matches the server and tool names exactly as written; spell the rule the way the tool is registered if they differ.`);
43645
+ const rules = permission[category] ??= {};
43646
+ if (isStricterAction({
43647
+ action,
43648
+ existing: rules[pattern]
43649
+ })) rules[pattern] = action;
43650
+ }
43651
+ }
43652
+ return permission;
43653
+ }
43654
+ /**
43655
+ * Permissions generator for Command Code.
43656
+ *
43657
+ * Rules live under the `permissions` key of `.commandcode/settings.json`
43658
+ * (project) and `~/.commandcode/settings.json` (user) as three lists of
43659
+ * Claude-style entries: `deny` (always wins), `ask`, `allow`. The same key
43660
+ * also carries `defaultMode`, `additionalDirectories` and `disableBypass`,
43661
+ * and the file holds `hooks` and other settings, so writes go through the
43662
+ * shared-config gateway: rulesync replaces the three lists, keeps every
43663
+ * sibling key, and never deletes the file.
43664
+ *
43665
+ * Generate: `permission.<category>.<pattern>` becomes `Tool(pattern)` with
43666
+ * the friendly names (`Shell`, `Read`, `Edit`, `Write`, `WebFetch`,
43667
+ * `WebSearch`), `mcp__<server>__<tool>` names pass through, and the all-tools
43668
+ * `*` category becomes the bare `*` rule Command Code accepts in `deny`/`ask`.
43669
+ * Only the entries of the categories the canonical config names are rebuilt;
43670
+ * every other existing entry is preserved verbatim.
43671
+ * Import: the lists are parsed back as what Command Code enforces for each
43672
+ * entry — friendly tool names case-insensitively (with the internal aliases
43673
+ * such as `Bash`, `PowerShell` and `write_file` folding onto their
43674
+ * category), MCP names exactly; rules for internal tool names rulesync does
43675
+ * not model are skipped.
43676
+ *
43677
+ * @see https://commandcode.ai/docs/permissions
43678
+ * @see https://commandcode.ai/docs/settings
43679
+ */
43680
+ var CommandcodePermissions = class CommandcodePermissions extends ToolPermissions {
43681
+ constructor(params) {
43682
+ super({
43683
+ ...params,
43684
+ fileContent: params.fileContent ?? "{}"
43685
+ });
43686
+ }
43687
+ /**
43688
+ * `settings.json` holds hooks and other user settings, so it is never
43689
+ * deleted; clearing permissions happens via an in-place merge.
43690
+ */
43691
+ isDeletable() {
43692
+ return false;
43693
+ }
43694
+ static getSettablePaths(_options = {}) {
43695
+ return {
43696
+ relativeDirPath: COMMANDCODE_DIR,
43697
+ relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
43698
+ };
43699
+ }
43700
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
43701
+ const paths = CommandcodePermissions.getSettablePaths({ global });
43702
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
43703
+ return new CommandcodePermissions({
43704
+ outputRoot,
43705
+ relativeDirPath: paths.relativeDirPath,
43706
+ relativeFilePath: paths.relativeFilePath,
43707
+ fileContent,
43708
+ validate,
43709
+ global
43710
+ });
43711
+ }
43712
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
43713
+ const paths = CommandcodePermissions.getSettablePaths({ global });
43714
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
43715
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
43716
+ const existing = parseCommandcodeSettings({
43717
+ fileContent: existingContent,
43718
+ relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
43719
+ });
43720
+ const existingPermissions = isRecord$1(existing[COMMANDCODE_PERMISSIONS_KEY]) ? existing[COMMANDCODE_PERMISSIONS_KEY] : {};
43721
+ const lists = buildCommandcodeRuleLists({
43722
+ config: rulesyncPermissions.getJson(),
43723
+ existingPermissions,
43724
+ logger
43725
+ });
43726
+ const permissions = { ...existingPermissions };
43727
+ for (const key of [
43728
+ "allow",
43729
+ "ask",
43730
+ "deny"
43731
+ ]) if (lists[key].length > 0) permissions[key] = lists[key];
43732
+ else delete permissions[key];
43733
+ return new CommandcodePermissions({
43734
+ outputRoot,
43735
+ relativeDirPath: paths.relativeDirPath,
43736
+ relativeFilePath: paths.relativeFilePath,
43737
+ fileContent: applySharedConfigPatch({
43738
+ fileKey: sharedConfigFileKey(paths),
43739
+ feature: "permissions",
43740
+ existingContent,
43741
+ patch: { [COMMANDCODE_PERMISSIONS_KEY]: permissions },
43742
+ filePath
43743
+ }),
43744
+ validate: true,
43745
+ global
43746
+ });
43747
+ }
43748
+ toRulesyncPermissions() {
43749
+ const settings = parseCommandcodeSettings({
43750
+ fileContent: this.getFileContent() || "{}",
43751
+ relativePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
43752
+ });
43753
+ const permissions = isRecord$1(settings[COMMANDCODE_PERMISSIONS_KEY]) ? settings[COMMANDCODE_PERMISSIONS_KEY] : {};
43754
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission: parseCommandcodeRuleLists(permissions) }, null, 2) });
43755
+ }
43756
+ validate() {
43757
+ return {
43758
+ success: true,
43759
+ error: null
43760
+ };
43761
+ }
43762
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
43763
+ return new CommandcodePermissions({
43764
+ outputRoot,
43765
+ relativeDirPath,
43766
+ relativeFilePath,
43767
+ fileContent: "{}",
43768
+ validate: false,
43769
+ global
43770
+ });
43771
+ }
43772
+ };
43773
+ /**
43774
+ * Fail closed on a syntax error or non-object root, matching the write path's
43775
+ * shared-config declaration, so a broken file is surfaced rather than
43776
+ * partially imported or overwritten.
43777
+ */
43778
+ function parseCommandcodeSettings({ fileContent, relativePath }) {
43779
+ try {
43780
+ return parseSharedConfig({
43781
+ format: "json",
43782
+ fileContent,
43783
+ filePath: relativePath,
43784
+ invalidRootPolicy: "error"
43785
+ });
43786
+ } catch (error) {
43787
+ throw new Error(`Failed to parse Command Code settings in ${relativePath}: ${formatError(error)}`, { cause: error });
43788
+ }
43789
+ }
43790
+ //#endregion
42611
43791
  //#region src/features/permissions/continue-permissions.ts
42612
43792
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
42613
43793
  const CATCH_ALL_PATTERN$5 = "*";
@@ -53369,6 +54549,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
53369
54549
  supportsImport: true
53370
54550
  }
53371
54551
  }],
54552
+ ["commandcode", {
54553
+ class: CommandcodePermissions,
54554
+ meta: {
54555
+ supportsProject: true,
54556
+ supportsGlobal: true,
54557
+ supportsImport: true
54558
+ }
54559
+ }],
53372
54560
  ["continue", {
53373
54561
  class: ContinuePermissions,
53374
54562
  meta: {
@@ -56135,6 +57323,154 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
56135
57323
  }
56136
57324
  };
56137
57325
  //#endregion
57326
+ //#region src/features/skills/commandcode-skill.ts
57327
+ const CommandcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
57328
+ name: zod_mini.z.string(),
57329
+ description: zod_mini.z.string()
57330
+ });
57331
+ /**
57332
+ * Represents a Command Code skill directory.
57333
+ *
57334
+ * Command Code discovers Anthropic-style `<name>/SKILL.md` directories under
57335
+ * `<project>/.commandcode/skills/` (project scope) and
57336
+ * `~/.commandcode/skills/` (user scope). The frontmatter requires `name`
57337
+ * (matching the directory name) and `description`; supporting files next to
57338
+ * `SKILL.md` are carried along.
57339
+ *
57340
+ * @see https://commandcode.ai/docs/skills
57341
+ */
57342
+ var CommandcodeSkill = class CommandcodeSkill extends ToolSkill {
57343
+ constructor({ outputRoot = process.cwd(), relativeDirPath = COMMANDCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
57344
+ super({
57345
+ outputRoot,
57346
+ relativeDirPath,
57347
+ dirName,
57348
+ mainFile: {
57349
+ name: SKILL_FILE_NAME,
57350
+ body,
57351
+ frontmatter: { ...frontmatter }
57352
+ },
57353
+ otherFiles,
57354
+ global
57355
+ });
57356
+ if (validate) {
57357
+ const result = this.validate();
57358
+ if (!result.success) throw result.error;
57359
+ }
57360
+ }
57361
+ static getSettablePaths(_options = {}) {
57362
+ return { relativeDirPath: COMMANDCODE_SKILLS_DIR_PATH };
57363
+ }
57364
+ getFrontmatter() {
57365
+ return CommandcodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
57366
+ }
57367
+ getBody() {
57368
+ return this.mainFile?.body ?? "";
57369
+ }
57370
+ validate() {
57371
+ if (!this.mainFile) return {
57372
+ success: false,
57373
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
57374
+ };
57375
+ const result = CommandcodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
57376
+ if (!result.success) return {
57377
+ success: false,
57378
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
57379
+ };
57380
+ if (result.data.name !== this.getDirName()) return {
57381
+ success: false,
57382
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: frontmatter name (${result.data.name}) must match directory name (${this.getDirName()})`)
57383
+ };
57384
+ return {
57385
+ success: true,
57386
+ error: null
57387
+ };
57388
+ }
57389
+ toRulesyncSkill() {
57390
+ const { name, description, ...commandcodeSection } = this.getFrontmatter();
57391
+ const rulesyncFrontmatter = {
57392
+ name,
57393
+ description,
57394
+ targets: ["*"],
57395
+ ...Object.keys(commandcodeSection).length > 0 && { commandcode: commandcodeSection }
57396
+ };
57397
+ return new RulesyncSkill({
57398
+ outputRoot: this.outputRoot,
57399
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
57400
+ dirName: this.getDirName(),
57401
+ frontmatter: rulesyncFrontmatter,
57402
+ body: this.getBody(),
57403
+ otherFiles: this.getOtherFiles(),
57404
+ validate: true,
57405
+ global: this.global
57406
+ });
57407
+ }
57408
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
57409
+ const settablePaths = CommandcodeSkill.getSettablePaths({ global });
57410
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
57411
+ const { name: _sectionName, description: _sectionDescription, ...commandcodeSection } = rulesyncFrontmatter.commandcode ?? {};
57412
+ const commandcodeFrontmatter = {
57413
+ ...commandcodeSection,
57414
+ name: rulesyncFrontmatter.name,
57415
+ description: rulesyncFrontmatter.description
57416
+ };
57417
+ return new CommandcodeSkill({
57418
+ outputRoot,
57419
+ relativeDirPath: settablePaths.relativeDirPath,
57420
+ dirName: commandcodeFrontmatter.name,
57421
+ frontmatter: commandcodeFrontmatter,
57422
+ body: rulesyncSkill.getBody(),
57423
+ otherFiles: rulesyncSkill.getOtherFiles(),
57424
+ validate,
57425
+ global
57426
+ });
57427
+ }
57428
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
57429
+ const targets = rulesyncSkill.getFrontmatter().targets;
57430
+ return targets.includes("*") || targets.includes("commandcode");
57431
+ }
57432
+ static async fromDir(params) {
57433
+ const loaded = await this.loadSkillDirContent({
57434
+ ...params,
57435
+ getSettablePaths: CommandcodeSkill.getSettablePaths
57436
+ });
57437
+ const result = CommandcodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
57438
+ if (!result.success) {
57439
+ const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
57440
+ throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
57441
+ }
57442
+ if (result.data.name !== loaded.dirName) {
57443
+ const skillFilePath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName, SKILL_FILE_NAME);
57444
+ throw new Error(`Frontmatter name (${result.data.name}) must match directory name (${loaded.dirName}) in ${skillFilePath}`);
57445
+ }
57446
+ return new CommandcodeSkill({
57447
+ outputRoot: loaded.outputRoot,
57448
+ relativeDirPath: loaded.relativeDirPath,
57449
+ dirName: loaded.dirName,
57450
+ frontmatter: result.data,
57451
+ body: loaded.body,
57452
+ otherFiles: loaded.otherFiles,
57453
+ validate: true,
57454
+ global: loaded.global
57455
+ });
57456
+ }
57457
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
57458
+ return new CommandcodeSkill({
57459
+ outputRoot,
57460
+ relativeDirPath,
57461
+ dirName,
57462
+ frontmatter: {
57463
+ name: "",
57464
+ description: ""
57465
+ },
57466
+ body: "",
57467
+ otherFiles: [],
57468
+ validate: false,
57469
+ global
57470
+ });
57471
+ }
57472
+ };
57473
+ //#endregion
56138
57474
  //#region src/features/skills/continue-skill.ts
56139
57475
  const ContinueSkillFrontmatterSchema = zod_mini.z.looseObject({
56140
57476
  name: zod_mini.z.string(),
@@ -61324,6 +62660,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
61324
62660
  supportsGlobal: true
61325
62661
  }
61326
62662
  }],
62663
+ ["commandcode", {
62664
+ class: CommandcodeSkill,
62665
+ meta: {
62666
+ supportsProject: true,
62667
+ supportsSimulated: false,
62668
+ supportsGlobal: true
62669
+ }
62670
+ }],
61327
62671
  ["continue", {
61328
62672
  class: ContinueSkill,
61329
62673
  meta: {
@@ -63427,6 +64771,143 @@ var CodexCliSubagent = class CodexCliSubagent extends ToolSubagent {
63427
64771
  }
63428
64772
  };
63429
64773
  //#endregion
64774
+ //#region src/features/subagents/commandcode-subagent.ts
64775
+ const CommandcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
64776
+ name: zod_mini.z.string(),
64777
+ description: zod_mini.z.optional(zod_mini.z.string()),
64778
+ tools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
64779
+ disallowedTools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
64780
+ model: zod_mini.z.optional(zod_mini.z.string()),
64781
+ reasoningEffort: zod_mini.z.optional(zod_mini.z.string()),
64782
+ maxTurns: zod_mini.z.optional(zod_mini.z.number()),
64783
+ permissionMode: zod_mini.z.optional(zod_mini.z.string()),
64784
+ background: zod_mini.z.optional(zod_mini.z.boolean()),
64785
+ showOutput: zod_mini.z.optional(zod_mini.z.boolean())
64786
+ });
64787
+ /**
64788
+ * Command Code custom agent: a Markdown file with YAML frontmatter under
64789
+ * `.commandcode/agents/` (project) or `~/.commandcode/agents/` (user); the
64790
+ * body is the agent's system prompt.
64791
+ *
64792
+ * @see https://commandcode.ai/docs/custom-agents
64793
+ */
64794
+ var CommandcodeSubagent = class CommandcodeSubagent extends ToolSubagent {
64795
+ frontmatter;
64796
+ body;
64797
+ constructor({ frontmatter, body, fileContent, ...rest }) {
64798
+ if (rest.validate !== false) {
64799
+ const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
64800
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
64801
+ }
64802
+ super({
64803
+ ...rest,
64804
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
64805
+ });
64806
+ this.frontmatter = frontmatter;
64807
+ this.body = body;
64808
+ }
64809
+ static getSettablePaths(_options = {}) {
64810
+ return { relativeDirPath: COMMANDCODE_AGENTS_DIR_PATH };
64811
+ }
64812
+ getFrontmatter() {
64813
+ return this.frontmatter;
64814
+ }
64815
+ getBody() {
64816
+ return this.body;
64817
+ }
64818
+ toRulesyncSubagent() {
64819
+ const { name, description, ...rest } = this.frontmatter;
64820
+ return new RulesyncSubagent({
64821
+ outputRoot: ".",
64822
+ frontmatter: {
64823
+ targets: ["*"],
64824
+ name,
64825
+ description,
64826
+ commandcode: { ...rest }
64827
+ },
64828
+ body: this.body,
64829
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
64830
+ relativeFilePath: this.getRelativeFilePath(),
64831
+ validate: true
64832
+ });
64833
+ }
64834
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
64835
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
64836
+ const commandcodeSection = rulesyncFrontmatter.commandcode ?? {};
64837
+ const commandcodeSubagentFrontmatter = {
64838
+ name: rulesyncFrontmatter.name,
64839
+ description: rulesyncFrontmatter.description,
64840
+ ...commandcodeSection
64841
+ };
64842
+ const body = rulesyncSubagent.getBody();
64843
+ const fileContent = stringifyFrontmatter(body, commandcodeSubagentFrontmatter, { avoidBlockScalars: true });
64844
+ const paths = this.getSettablePaths({ global });
64845
+ return new CommandcodeSubagent({
64846
+ outputRoot,
64847
+ frontmatter: commandcodeSubagentFrontmatter,
64848
+ body,
64849
+ relativeDirPath: paths.relativeDirPath,
64850
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
64851
+ fileContent,
64852
+ validate,
64853
+ global
64854
+ });
64855
+ }
64856
+ validate() {
64857
+ if (!this.frontmatter) return {
64858
+ success: true,
64859
+ error: null
64860
+ };
64861
+ const result = CommandcodeSubagentFrontmatterSchema.safeParse(this.frontmatter);
64862
+ if (result.success) return {
64863
+ success: true,
64864
+ error: null
64865
+ };
64866
+ else return {
64867
+ success: false,
64868
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
64869
+ };
64870
+ }
64871
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
64872
+ return this.isTargetedByRulesyncSubagentDefault({
64873
+ rulesyncSubagent,
64874
+ toolTarget: "commandcode"
64875
+ });
64876
+ }
64877
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
64878
+ const paths = this.getSettablePaths({ global });
64879
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
64880
+ const fileContent = await readFileContent(filePath);
64881
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
64882
+ const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
64883
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
64884
+ return new CommandcodeSubagent({
64885
+ outputRoot,
64886
+ relativeDirPath: paths.relativeDirPath,
64887
+ relativeFilePath,
64888
+ frontmatter: result.data,
64889
+ body: content.trim(),
64890
+ fileContent,
64891
+ validate,
64892
+ global
64893
+ });
64894
+ }
64895
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
64896
+ return new CommandcodeSubagent({
64897
+ outputRoot,
64898
+ relativeDirPath,
64899
+ relativeFilePath,
64900
+ frontmatter: {
64901
+ name: "",
64902
+ description: ""
64903
+ },
64904
+ body: "",
64905
+ fileContent: "",
64906
+ validate: false
64907
+ });
64908
+ }
64909
+ };
64910
+ //#endregion
63430
64911
  //#region src/features/subagents/copilot-subagent.ts
63431
64912
  const CopilotSubagentFrontmatterSchema = zod_mini.z.looseObject({
63432
64913
  name: zod_mini.z.string(),
@@ -67085,6 +68566,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
67085
68566
  filePattern: "*.toml"
67086
68567
  }
67087
68568
  }],
68569
+ ["commandcode", {
68570
+ class: CommandcodeSubagent,
68571
+ meta: {
68572
+ supportsProject: true,
68573
+ supportsSimulated: false,
68574
+ supportsGlobal: true,
68575
+ filePattern: "*.md"
68576
+ }
68577
+ }],
67088
68578
  ["copilot", {
67089
68579
  class: CopilotSubagent,
67090
68580
  meta: {
@@ -69917,6 +71407,73 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
69917
71407
  }
69918
71408
  };
69919
71409
  //#endregion
71410
+ //#region src/features/rules/commandcode-rule.ts
71411
+ var CommandcodeRule = class CommandcodeRule extends ToolRule {
71412
+ constructor({ fileContent, root, ...rest }) {
71413
+ super({
71414
+ ...rest,
71415
+ fileContent,
71416
+ root: root ?? false
71417
+ });
71418
+ }
71419
+ static getSettablePaths({ global = false } = {}) {
71420
+ return { root: {
71421
+ relativeDirPath: global ? COMMANDCODE_DIR : ".",
71422
+ relativeFilePath: COMMANDCODE_RULE_FILE_NAME
71423
+ } };
71424
+ }
71425
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
71426
+ const { root } = this.getSettablePaths({ global });
71427
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
71428
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
71429
+ return new CommandcodeRule({
71430
+ outputRoot,
71431
+ relativeDirPath: root.relativeDirPath,
71432
+ relativeFilePath: root.relativeFilePath,
71433
+ fileContent,
71434
+ validate,
71435
+ root: true
71436
+ });
71437
+ }
71438
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
71439
+ const { root } = this.getSettablePaths({ global });
71440
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
71441
+ return new CommandcodeRule({
71442
+ outputRoot,
71443
+ relativeDirPath: root.relativeDirPath,
71444
+ relativeFilePath: root.relativeFilePath,
71445
+ fileContent: rulesyncRule.getBody(),
71446
+ validate,
71447
+ root: isRoot
71448
+ });
71449
+ }
71450
+ toRulesyncRule() {
71451
+ return this.toRulesyncRuleDefault();
71452
+ }
71453
+ validate() {
71454
+ return {
71455
+ success: true,
71456
+ error: null
71457
+ };
71458
+ }
71459
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
71460
+ return new CommandcodeRule({
71461
+ outputRoot,
71462
+ relativeDirPath,
71463
+ relativeFilePath,
71464
+ fileContent: "",
71465
+ validate: false,
71466
+ root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".commandcode")
71467
+ });
71468
+ }
71469
+ static isTargetedByRulesyncRule(rulesyncRule) {
71470
+ return this.isTargetedByRulesyncRuleDefault({
71471
+ rulesyncRule,
71472
+ toolTarget: "commandcode"
71473
+ });
71474
+ }
71475
+ };
71476
+ //#endregion
69920
71477
  //#region src/features/rules/continue-rule.ts
69921
71478
  /**
69922
71479
  * Frontmatter schema for Continue rule files (`.continue/rules/*.md`).
@@ -74237,6 +75794,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
74237
75794
  collisionPolicy: "fold"
74238
75795
  }
74239
75796
  }],
75797
+ ["commandcode", {
75798
+ class: CommandcodeRule,
75799
+ meta: {
75800
+ extension: "md",
75801
+ supportsGlobal: true,
75802
+ ruleDiscoveryMode: "auto",
75803
+ collisionPolicy: "fold"
75804
+ }
75805
+ }],
74240
75806
  ["continue", {
74241
75807
  class: ContinueRule,
74242
75808
  meta: {