rulesync 16.33.0 → 16.34.1

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: {
@@ -30729,6 +31034,17 @@ function toCanonicalMcpServers(servers) {
30729
31034
  return renameServerField(servers, "serverUrl", "url");
30730
31035
  }
30731
31036
  /**
31037
+ * Parse `mcp_config.json` as JSONC and require a top-level object, so a file
31038
+ * whose root is `null`, an array or a primitive fails with a clear message
31039
+ * instead of a `TypeError` on `mcpServers` access or an array being spread
31040
+ * into the rewritten config.
31041
+ */
31042
+ function parseAntigravityMcpConfig(fileContent) {
31043
+ const parsed = parseJsonc$6(fileContent);
31044
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Antigravity ${ANTIGRAVITY_MCP_FILE_NAME} must contain a top-level JSON object`);
31045
+ return { ...parsed };
31046
+ }
31047
+ /**
30732
31048
  * Shared MCP generator for Google Antigravity (Antigravity 2.0), used by both
30733
31049
  * the IDE and the CLI.
30734
31050
  *
@@ -30741,12 +31057,17 @@ function toCanonicalMcpServers(servers) {
30741
31057
  * The IDE and CLI share the project path (`.agents/mcp_config.json`) and differ
30742
31058
  * only in their global config subdirectory, which each concrete subclass
30743
31059
  * supplies via {@link AntigravityMcp.getGlobalSubdir}.
31060
+ *
31061
+ * Antigravity CLI 1.1.24+ accepts line and block comments and trailing
31062
+ * commas in `mcp_config.json`, so the file is read as JSONC. Writes stay plain
31063
+ * JSON (comments are dropped on rewrite), matching the other JSONC-reading
31064
+ * adapters.
30744
31065
  */
30745
31066
  var AntigravityMcp = class extends ToolMcp {
30746
31067
  json;
30747
31068
  constructor(params) {
30748
31069
  super(params);
30749
- this.json = JSON.parse(this.fileContent || "{}");
31070
+ this.json = parseAntigravityMcpConfig(this.fileContent || "{}");
30750
31071
  }
30751
31072
  getJson() {
30752
31073
  return this.json;
@@ -30767,8 +31088,7 @@ var AntigravityMcp = class extends ToolMcp {
30767
31088
  }
30768
31089
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30769
31090
  const paths = this.getSettablePaths({ global });
30770
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
30771
- const json = JSON.parse(fileContent);
31091
+ const json = parseAntigravityMcpConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}");
30772
31092
  const newJson = {
30773
31093
  ...json,
30774
31094
  mcpServers: json.mcpServers ?? {}
@@ -30784,9 +31104,8 @@ var AntigravityMcp = class extends ToolMcp {
30784
31104
  }
30785
31105
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
30786
31106
  const paths = this.getSettablePaths({ global });
30787
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2);
30788
31107
  const newJson = {
30789
- ...JSON.parse(fileContent),
31108
+ ...parseAntigravityMcpConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2)),
30790
31109
  mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers())
30791
31110
  };
30792
31111
  return new this({
@@ -31956,6 +32275,254 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
31956
32275
  }
31957
32276
  };
31958
32277
  //#endregion
32278
+ //#region src/features/mcp/commandcode-mcp.ts
32279
+ /**
32280
+ * Parse a Command Code MCP file. The project file is the `.mcp.json` other
32281
+ * agents (Claude Code among them) share, so a hand-written file is read as
32282
+ * JSONC to tolerate comments and trailing commas; malformed content or a
32283
+ * non-object root (`null`, an array, a scalar) fails closed rather than being
32284
+ * spread into the regenerated file.
32285
+ */
32286
+ function parseCommandcodeMcpConfig({ fileContent, relativePath }) {
32287
+ let parsed;
32288
+ try {
32289
+ parsed = parseJsonc$6(fileContent);
32290
+ } catch (error) {
32291
+ throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: ${formatError(error)}`, { cause: error });
32292
+ }
32293
+ if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: expected a JSON object at the root`);
32294
+ return parsed;
32295
+ }
32296
+ /**
32297
+ * The remote transport Command Code reads a server as. Its loader accepts
32298
+ * `http` (streamable HTTP) and `sse` for a `url` server; a bare `url` defaults
32299
+ * to `http`, and the canonical `streamable-http` spelling is folded into it.
32300
+ * A `ws(s)://` URL or any other stated transport has no Command Code
32301
+ * equivalent, so `undefined` tells the caller to skip the server.
32302
+ * @see https://commandcode.ai/docs/mcp
32303
+ */
32304
+ function asCommandcodeRemoteTransport(stated, url) {
32305
+ if (stated === "sse") return "sse";
32306
+ if (stated === "http" || stated === "streamable-http") return "http";
32307
+ if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
32308
+ }
32309
+ /**
32310
+ * Convert the canonical server map to the shape Command Code's config schema
32311
+ * documents (used for the global `~/.commandcode/mcp.json` only — the project
32312
+ * `.mcp.json` is shared with Claude Code and written pass-through, see the
32313
+ * class doc): a remote server is `{ transport, url, headers?, env?, oauth? }`
32314
+ * and a stdio server is `{ transport: "stdio", command, args?, env? }`, each
32315
+ * with an optional `enabled` flag. The documented spelling is `transport`
32316
+ * (`type` is only accepted as an alias on read), so that is what is written,
32317
+ * and only the documented keys are emitted — `timeout`, rulesync-only fields
32318
+ * and anything else is left out. The canonical `disabled: true` becomes
32319
+ * Command Code's native `enabled: false` (the rulesync-source-only `enabled`
32320
+ * filter never reaches this function: `getMcpServers()` drops such a server).
32321
+ *
32322
+ * A server Command Code drops at load time — no transport at all, a remote
32323
+ * transport without a URL, a WebSocket URL, or a stdio entry without a
32324
+ * command — is skipped with a warning rather than written in a form the tool
32325
+ * would silently ignore.
32326
+ * @see https://commandcode.ai/docs/mcp
32327
+ */
32328
+ function convertToCommandcodeFormat(mcpServers, logger) {
32329
+ const result = {};
32330
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
32331
+ if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
32332
+ if (declaresNoTransport(serverConfig)) {
32333
+ warnAndSkipMcpServer({
32334
+ toolName: "Command Code",
32335
+ serverName,
32336
+ reason: "no transport",
32337
+ logger
32338
+ });
32339
+ continue;
32340
+ }
32341
+ const converted = isRemoteMcpServer(serverConfig) ? convertRemoteServer$1({
32342
+ serverName,
32343
+ serverConfig,
32344
+ logger
32345
+ }) : convertStdioServer$1({
32346
+ serverName,
32347
+ serverConfig,
32348
+ logger
32349
+ });
32350
+ if (converted === void 0) continue;
32351
+ if (serverConfig.disabled === true) converted.enabled = false;
32352
+ if (isRecord$1(serverConfig.env)) converted.env = omitPrototypePollutionKeys(serverConfig.env);
32353
+ result[serverName] = converted;
32354
+ }
32355
+ return result;
32356
+ }
32357
+ function convertRemoteServer$1({ serverName, serverConfig, logger }) {
32358
+ const url = resolveRemoteMcpUrl(serverConfig);
32359
+ if (!url) {
32360
+ warnAndSkipMcpServer({
32361
+ toolName: "Command Code",
32362
+ serverName,
32363
+ reason: "a remote transport without a url",
32364
+ logger
32365
+ });
32366
+ return;
32367
+ }
32368
+ const stated = serverConfig.type ?? serverConfig.transport;
32369
+ const transport = asCommandcodeRemoteTransport(typeof stated === "string" ? stated : void 0, url);
32370
+ if (transport === void 0) {
32371
+ warnAndSkipMcpServer({
32372
+ toolName: "Command Code",
32373
+ serverName,
32374
+ 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)`,
32375
+ logger
32376
+ });
32377
+ return;
32378
+ }
32379
+ const converted = {
32380
+ transport,
32381
+ url
32382
+ };
32383
+ if (isRecord$1(serverConfig.headers)) converted.headers = omitPrototypePollutionKeys(serverConfig.headers);
32384
+ if (isRecord$1(serverConfig.oauth)) converted.oauth = omitPrototypePollutionKeys(serverConfig.oauth);
32385
+ return converted;
32386
+ }
32387
+ function convertStdioServer$1({ serverName, serverConfig, logger }) {
32388
+ const [command, ...args] = resolveLocalMcpCommand(serverConfig);
32389
+ if (!command) {
32390
+ warnAndSkipMcpServer({
32391
+ toolName: "Command Code",
32392
+ serverName,
32393
+ reason: "a stdio transport without a command",
32394
+ logger
32395
+ });
32396
+ return;
32397
+ }
32398
+ const converted = {
32399
+ transport: "stdio",
32400
+ command
32401
+ };
32402
+ if (args.length > 0) converted.args = args;
32403
+ return converted;
32404
+ }
32405
+ /**
32406
+ * Convert Command Code's server map back to the canonical shape. Both
32407
+ * spellings Command Code accepts (`transport` and its `type` alias, with
32408
+ * `command`/`url`) are already canonical, so entries pass through with only
32409
+ * prototype-pollution keys dropped; the native `enabled: false` maps to the
32410
+ * canonical `disabled: true` (a bare `enabled` would otherwise be read back
32411
+ * as rulesync's own generation filter and silently drop the server from every
32412
+ * other target).
32413
+ */
32414
+ function convertFromCommandcodeFormat(mcpServers) {
32415
+ if (!isMcpServers(mcpServers)) return {};
32416
+ const result = {};
32417
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
32418
+ if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
32419
+ const { enabled, ...rest } = omitPrototypePollutionKeys(serverConfig);
32420
+ result[serverName] = enabled === false ? {
32421
+ ...rest,
32422
+ disabled: true
32423
+ } : rest;
32424
+ }
32425
+ return result;
32426
+ }
32427
+ /**
32428
+ * Command Code MCP configuration.
32429
+ *
32430
+ * Command Code reads `.mcp.json` at the project root (project scope, meant to
32431
+ * be committed) and `~/.commandcode/mcp.json` (user scope), both in the
32432
+ * `{ "mcpServers": { ... } }` shape the docs show; the per-machine local
32433
+ * scope under `~/.commandcode/projects/` is left to the tool. Top-level
32434
+ * sibling keys of an existing file are kept. The global file is not deleted
32435
+ * by `--delete` (it lives outside the project), while the project file is
32436
+ * rulesync's own and is.
32437
+ *
32438
+ * The project `.mcp.json` is the very file the `claudecode` target writes, so
32439
+ * at project scope the servers are written in the same pass-through shape
32440
+ * `ClaudecodeMcp` uses: whichever of the two targets generates last leaves
32441
+ * byte-identical content, and Command Code reads that shape natively (`type`
32442
+ * is an alias of `transport`, a bare `url` infers `http`, and unknown keys
32443
+ * are ignored). Only the global file — Command Code's own — gets the
32444
+ * `transport` / `enabled: false` rewrite of `convertToCommandcodeFormat`.
32445
+ *
32446
+ * @see https://commandcode.ai/docs/mcp
32447
+ */
32448
+ var CommandcodeMcp = class CommandcodeMcp extends ToolMcp {
32449
+ json;
32450
+ constructor(params) {
32451
+ super(params);
32452
+ this.json = this.fileContent === void 0 ? {} : parseCommandcodeMcpConfig({
32453
+ fileContent: this.fileContent,
32454
+ relativePath: (0, node_path.join)(this.relativeDirPath, this.relativeFilePath)
32455
+ });
32456
+ }
32457
+ getJson() {
32458
+ return this.json;
32459
+ }
32460
+ isDeletable() {
32461
+ return !this.global;
32462
+ }
32463
+ static getSettablePaths({ global = false } = {}) {
32464
+ return global ? {
32465
+ relativeDirPath: COMMANDCODE_DIR,
32466
+ relativeFilePath: COMMANDCODE_GLOBAL_MCP_FILE_NAME
32467
+ } : {
32468
+ relativeDirPath: ".",
32469
+ relativeFilePath: COMMANDCODE_PROJECT_MCP_FILE_NAME
32470
+ };
32471
+ }
32472
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
32473
+ const paths = this.getSettablePaths({ global });
32474
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
32475
+ return new CommandcodeMcp({
32476
+ outputRoot,
32477
+ relativeDirPath: paths.relativeDirPath,
32478
+ relativeFilePath: paths.relativeFilePath,
32479
+ fileContent,
32480
+ validate,
32481
+ global
32482
+ });
32483
+ }
32484
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
32485
+ const paths = this.getSettablePaths({ global });
32486
+ const json = parseCommandcodeMcpConfig({
32487
+ fileContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
32488
+ relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
32489
+ });
32490
+ const mcpServers = global ? convertToCommandcodeFormat(rulesyncMcp.getMcpServers(), logger) : rulesyncMcp.getMcpServers();
32491
+ const commandcodeConfig = {
32492
+ ...json,
32493
+ mcpServers
32494
+ };
32495
+ return new CommandcodeMcp({
32496
+ outputRoot,
32497
+ relativeDirPath: paths.relativeDirPath,
32498
+ relativeFilePath: paths.relativeFilePath,
32499
+ fileContent: JSON.stringify(commandcodeConfig, null, 2),
32500
+ validate,
32501
+ global
32502
+ });
32503
+ }
32504
+ toRulesyncMcp() {
32505
+ const mcpServers = convertFromCommandcodeFormat(this.json.mcpServers);
32506
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
32507
+ }
32508
+ validate() {
32509
+ return {
32510
+ success: true,
32511
+ error: null
32512
+ };
32513
+ }
32514
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
32515
+ return new CommandcodeMcp({
32516
+ outputRoot,
32517
+ relativeDirPath,
32518
+ relativeFilePath,
32519
+ fileContent: "{}",
32520
+ validate: false,
32521
+ global
32522
+ });
32523
+ }
32524
+ };
32525
+ //#endregion
31959
32526
  //#region src/features/mcp/continue-mcp.ts
31960
32527
  /**
31961
32528
  * Parse a Continue MCP file. Continue reads the files under `mcpServers/` as
@@ -36924,6 +37491,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
36924
37491
  static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
36925
37492
  const servers = rulesyncMcp.forTarget({
36926
37493
  toolTarget: "rovodev",
37494
+ global,
36927
37495
  logger
36928
37496
  }).getMcpServers();
36929
37497
  const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
@@ -38221,6 +38789,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
38221
38789
  supportsDisabledTools: true
38222
38790
  }
38223
38791
  }],
38792
+ ["commandcode", {
38793
+ class: CommandcodeMcp,
38794
+ meta: {
38795
+ supportsProject: true,
38796
+ supportsGlobal: true,
38797
+ supportsEnabledTools: false,
38798
+ supportsDisabledTools: false
38799
+ }
38800
+ }],
38224
38801
  ["continue", {
38225
38802
  class: ContinueMcp,
38226
38803
  meta: {
@@ -38614,6 +39191,7 @@ var McpProcessor = class extends FeatureProcessor {
38614
39191
  const toolMcps = await Promise.all([rulesyncMcp].map(async (mcp) => {
38615
39192
  const targetedRulesyncMcp = mcp.forTarget({
38616
39193
  toolTarget: this.toolTarget,
39194
+ global: this.global,
38617
39195
  logger: this.logger
38618
39196
  });
38619
39197
  const fieldsToStrip = [];
@@ -41297,15 +41875,28 @@ const CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
41297
41875
  *
41298
41876
  * Derived from the per-key **Scope** column of the settings reference: every
41299
41877
  * top-level key documented as `User or managed` or `User, local, or managed`.
41878
+ * `enableArtifact` is deliberately absent: it is documented as `Any file` —
41879
+ * every file can turn the Artifact tool off, and none can turn it back on — so
41880
+ * a project file does honor it. `processWrapper` is also `User or managed`, but it is refused
41881
+ * in both scopes by `CLAUDECODE_COMMAND_EXECUTING_KEYS`, which runs first.
41882
+ *
41883
+ * `autoContinueAtUsageLimit` is dropped at project scope for a stronger reason
41884
+ * than the others: when no user or managed file sets it, a project or local
41885
+ * value turns the feature *off* whatever the value is, so committing a `true`
41886
+ * would silently do the opposite of what it says.
41300
41887
  *
41301
41888
  * @see https://code.claude.com/docs/en/settings-reference
41302
41889
  */
41303
41890
  const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
41304
41891
  "askUserQuestionTimeout",
41892
+ "autoContinueAtUsageLimit",
41305
41893
  "autoMode",
41894
+ "bashEditDiffEnabled",
41895
+ "desktopSessionCleanupPeriodDays",
41306
41896
  "dialogExpiry",
41307
- "enableArtifact",
41897
+ "feedbackDrafts",
41308
41898
  "footerLinksRegexes",
41899
+ "modelPicker",
41309
41900
  "pluginConfigs",
41310
41901
  "skipAutoPermissionPrompt",
41311
41902
  "skipDangerousModePermissionPrompt",
@@ -41338,14 +41929,20 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
41338
41929
  browserExternalPageTools: "managed settings",
41339
41930
  channelsEnabled: "managed settings",
41340
41931
  claudeMd: "managed settings",
41932
+ copyOnSelect: "~/.claude.json",
41341
41933
  diffTool: "~/.claude.json",
41342
41934
  disableBrowserExternalNavigation: "managed settings",
41343
41935
  disableCommandPluginSources: "managed settings",
41936
+ disableDesktopLocalSessions: "managed settings",
41344
41937
  disableMobileSimulatorTools: "managed settings",
41345
41938
  disableSideloadFlags: "managed settings",
41346
41939
  externalEditorContext: "~/.claude.json",
41347
41940
  forceLoginGatewayUrl: "managed settings",
41348
41941
  forceRemoteSettingsRefresh: "managed settings",
41942
+ gatewayInternalNetworks: "managed settings",
41943
+ managedMcpServers: "managed settings",
41944
+ managedSourcesBehavior: "managed settings",
41945
+ modelPricing: "managed settings",
41349
41946
  parentSettingsBehavior: "managed settings",
41350
41947
  permissionExplainerEnabled: "~/.claude.json",
41351
41948
  pluginSuggestionMarketplaces: "managed settings",
@@ -42608,6 +43205,622 @@ function mapBashActionToDecision(action) {
42608
43205
  return "forbidden";
42609
43206
  }
42610
43207
  //#endregion
43208
+ //#region src/features/permissions/commandcode-permissions.ts
43209
+ /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
43210
+ const COMMANDCODE_PERMISSIONS_KEY = "permissions";
43211
+ const CATCH_ALL_PATTERN$6 = "*";
43212
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
43213
+ const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
43214
+ const CATEGORY_TO_COMMANDCODE_TOOL = {
43215
+ bash: "Shell",
43216
+ read: "Read",
43217
+ edit: "Edit",
43218
+ write: "Write",
43219
+ grep: "Grep",
43220
+ glob: "Glob",
43221
+ webfetch: "WebFetch",
43222
+ websearch: "WebSearch"
43223
+ };
43224
+ const COMMANDCODE_TOOL_TO_CATEGORY = {
43225
+ ...Object.fromEntries(Object.entries(CATEGORY_TO_COMMANDCODE_TOOL).map(([category, tool]) => [tool.toLowerCase(), category])),
43226
+ bash: "bash",
43227
+ powershell: "bash",
43228
+ shell_command: "bash",
43229
+ monitor_command: "bash",
43230
+ kill_shell: "bash",
43231
+ notebookedit: "edit",
43232
+ write_file: "write",
43233
+ web_fetch: "webfetch",
43234
+ web_search: "websearch"
43235
+ };
43236
+ const ACTION_RANK$1 = {
43237
+ allow: 0,
43238
+ ask: 1,
43239
+ deny: 2
43240
+ };
43241
+ /** Whether `action` outranks `existing` (deny > ask > allow). */
43242
+ function isStricterAction({ action, existing }) {
43243
+ return existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing];
43244
+ }
43245
+ /**
43246
+ * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
43247
+ * the whitespace makes it a pattern rule, and a shell pattern grants only
43248
+ * commands that pass Command Code's pattern gate.
43249
+ */
43250
+ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (the command must parse into words and carry no environment assignment)";
43251
+ /**
43252
+ * Build a Command Code rule (`Shell(git *)`, `Read`, `mcp__github__get_issue`,
43253
+ * `mcp__*`, `*`) from a canonical category + pattern. Returns `null` for
43254
+ * categories Command Code cannot express so the caller can skip them.
43255
+ *
43256
+ * MCP tools are keyed by their canonical `mcp__server__tool` name, so a
43257
+ * scoped `mcp__<remainder>` category is written verbatim and the bare `mcp`
43258
+ * category becomes `mcp__*` (or `mcp__<pattern>` when the pattern names a
43259
+ * server or tool). A pattern on an MCP category is still written as the
43260
+ * `(specifier)` here; `writableCommandcodeRule` decides what to do with it,
43261
+ * because Command Code ignores it. The all-tools `*` category is only
43262
+ * written for its catch-all pattern; narrower `*` patterns are shell
43263
+ * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
43264
+ */
43265
+ function buildCommandcodeRule(category, pattern) {
43266
+ const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
43267
+ if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
43268
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
43269
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
43270
+ const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
43271
+ if (tool === void 0) return null;
43272
+ return catchAll ? tool : `${tool}(${pattern})`;
43273
+ }
43274
+ /**
43275
+ * Split `Tool(specifier)` into its two halves the way Command Code's
43276
+ * `splitRule` does (`command-code` 1.54.0): the rule is split at the first
43277
+ * unescaped `(` and must end with `)`, the tool half is trimmed (so
43278
+ * `Shell (rm -rf *)` is a `Shell` rule), and a `\(` / `\)` in the specifier
43279
+ * is an escaped parenthesis. `Tool` alone has an empty specifier. A rule that
43280
+ * opens a parenthesis without closing one is not a rule to Command Code at
43281
+ * all, so it comes back with an empty tool half and stays unmodeled. Nothing
43282
+ * else in the specifier is touched: Command Code does not trim it (the shell
43283
+ * matcher normalizes whitespace on its own, see `parseCommandcodeRule`), so
43284
+ * `Read( )` is a rule for the pattern `" "`, which matches nothing, not for
43285
+ * the whole tool.
43286
+ */
43287
+ function splitCommandcodeRule(rule) {
43288
+ const trimmed = rule.trim();
43289
+ const parenIndex = trimmed.search(/(?<!\\)\(/);
43290
+ if (parenIndex === -1) return {
43291
+ tool: trimmed,
43292
+ inner: ""
43293
+ };
43294
+ if (!trimmed.endsWith(")")) return {
43295
+ tool: "",
43296
+ inner: ""
43297
+ };
43298
+ return {
43299
+ tool: trimmed.slice(0, parenIndex).trim(),
43300
+ inner: trimmed.slice(parenIndex + 1, -1).replace(/\\([()])/g, "$1")
43301
+ };
43302
+ }
43303
+ /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
43304
+ function isCatchAllSpecifier(inner) {
43305
+ return inner === "" || inner === CATCH_ALL_PATTERN$6;
43306
+ }
43307
+ /**
43308
+ * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
43309
+ * `parsePermissionRule` / `parseMcpToken`). The MCP shape is only recognized
43310
+ * by its exact `mcp__` prefix, and such a rule never carries a specifier:
43311
+ * `mcp__<server>__<tool>(owner:foo)` denies, asks or allows the whole tool.
43312
+ * Its names are matched case-sensitively against the registered server and
43313
+ * tool (Command Code never lowercases them), so the remainder keeps its case
43314
+ * as the canonical category. `mcp__<server>`, `mcp__<server>__` and
43315
+ * `mcp__<server>__*` are the whole server (category `mcp__<server>`), and a
43316
+ * `*` server (`mcp__*`, `mcp__*__<tool>`) is every MCP tool, honored in
43317
+ * `deny`/`ask` only. A differently-cased `MCP__...` spelling is read as a
43318
+ * plain tool-name rule instead, matched case-insensitively:
43319
+ * `MCP__<server>__<tool>` matches that tool by name (so it folds onto the
43320
+ * `mcp__` category, remainder as written), `MCP__<server>__<tool>(specifier)`
43321
+ * globs the specifier against the call's `command` / `file_path` / `path` /
43322
+ * `url` / `pattern` argument (in `deny`/`ask` a `param:glob` form is tried
43323
+ * first) — kept as a pattern in `deny`/`ask`, but not imported from `allow`,
43324
+ * where a grant scoped that way cannot be modeled without widening it; the
43325
+ * globs
43326
+ * `MCP__*` (every MCP tool) and `MCP__<server>__*` (the whole server) match
43327
+ * by name in `deny`/`ask` only, and `MCP__<server>` names no tool at all.
43328
+ * Whatever matches nothing anywhere (or a glob rulesync does not model)
43329
+ * returns `null` and is left alone.
43330
+ */
43331
+ function parseMcpCommandcodeRule({ tool, inner }) {
43332
+ const remainder = tool.slice(5);
43333
+ const separator = remainder.indexOf("__");
43334
+ const server = separator === -1 ? remainder : remainder.slice(0, separator);
43335
+ const rest = separator === -1 ? "" : remainder.slice(separator + 2);
43336
+ if (server.length === 0) return null;
43337
+ const everyServer = {
43338
+ category: "mcp",
43339
+ pattern: CATCH_ALL_PATTERN$6,
43340
+ notInAllow: true
43341
+ };
43342
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
43343
+ if (server === CATCH_ALL_PATTERN$6) return everyServer;
43344
+ if (server.includes(CATCH_ALL_PATTERN$6)) return null;
43345
+ return {
43346
+ category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
43347
+ pattern: CATCH_ALL_PATTERN$6,
43348
+ notInAllow: false
43349
+ };
43350
+ }
43351
+ const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
43352
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
43353
+ if (!isCatchAllSpecifier(inner)) return namesTool ? {
43354
+ category,
43355
+ pattern: inner,
43356
+ notInAllow: true
43357
+ } : null;
43358
+ if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
43359
+ if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
43360
+ category: `${MCP_CANONICAL_PREFIX$5}${server}`,
43361
+ pattern: CATCH_ALL_PATTERN$6,
43362
+ notInAllow: true
43363
+ };
43364
+ return namesTool ? {
43365
+ category,
43366
+ pattern: CATCH_ALL_PATTERN$6,
43367
+ notInAllow: false
43368
+ } : null;
43369
+ }
43370
+ /**
43371
+ * A rule on one of the friendly tools, with its specifier read as Command
43372
+ * Code matches it. The shell matcher trims and collapses whitespace on both
43373
+ * the pattern and the command, so `Shell( git * )` is `Shell(git *)` and a
43374
+ * pattern that is blank once normalized (`Shell( )`), or a bare `:*` whose
43375
+ * prefix is empty (`Shell(:*)`), matches nothing. A padded `Shell( * )` is
43376
+ * not the bare `Shell`: only a specifier of exactly
43377
+ * `` or `*` is dropped by the rule parser, so `( * )` stays a pattern rule —
43378
+ * in `deny`/`ask` it matches every command (the whole tool), while in `allow`
43379
+ * it is narrower than the bare `Shell` (it needs a command that parses into
43380
+ * words and has no environment-assignment prefix), so it is the whole tool
43381
+ * marked `notInAllow`. The path, web and tool-name matchers use the
43382
+ * specifier as written, so one that is blank or `*` only once trimmed
43383
+ * (`Read( * )`) matches nothing. What matches nothing comes back as `null`:
43384
+ * the rule is unmodeled and left alone.
43385
+ */
43386
+ function friendlyToolRule({ category, inner }) {
43387
+ if (category === "bash") {
43388
+ if (isCatchAllSpecifier(inner)) return {
43389
+ category,
43390
+ pattern: CATCH_ALL_PATTERN$6,
43391
+ notInAllow: false
43392
+ };
43393
+ const normalized = inner.trim().replace(/\s+/g, " ");
43394
+ if (normalized === "" || normalized === ":*") return null;
43395
+ return isCatchAllSpecifier(normalized) ? {
43396
+ category,
43397
+ pattern: CATCH_ALL_PATTERN$6,
43398
+ notInAllow: true
43399
+ } : {
43400
+ category,
43401
+ pattern: normalized,
43402
+ notInAllow: false
43403
+ };
43404
+ }
43405
+ if (isCatchAllSpecifier(inner)) return {
43406
+ category,
43407
+ pattern: CATCH_ALL_PATTERN$6,
43408
+ notInAllow: false
43409
+ };
43410
+ return isCatchAllSpecifier(inner.trim()) ? null : {
43411
+ category,
43412
+ pattern: inner,
43413
+ notInAllow: false
43414
+ };
43415
+ }
43416
+ /**
43417
+ * Parse a Command Code rule back into canonical terms. Tool names fold case;
43418
+ * `Tool`, `Tool()` and `Tool(*)` all mean the whole tool; MCP-shaped rules
43419
+ * go through `parseMcpCommandcodeRule`. Returns `null` for a rule rulesync
43420
+ * cannot model (an exact internal tool name such as `edit_file`, a
43421
+ * name-wildcard like `edit_*`, a specifier on a rule that takes none, or a
43422
+ * rule Command Code enforces nothing for, such as `Agent`).
43423
+ */
43424
+ function parseCommandcodeRule(rule) {
43425
+ const { tool, inner } = splitCommandcodeRule(rule);
43426
+ if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
43427
+ category: "*",
43428
+ pattern: CATCH_ALL_PATTERN$6,
43429
+ notInAllow: true
43430
+ } : null;
43431
+ const lowered = tool.toLowerCase();
43432
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
43433
+ tool,
43434
+ inner
43435
+ });
43436
+ const category = Object.hasOwn(COMMANDCODE_TOOL_TO_CATEGORY, lowered) ? COMMANDCODE_TOOL_TO_CATEGORY[lowered] : void 0;
43437
+ if (category === void 0) return null;
43438
+ return friendlyToolRule({
43439
+ category,
43440
+ inner
43441
+ });
43442
+ }
43443
+ /**
43444
+ * Whether an imported rule is a differently-cased `MCP__<server>__<tool>`
43445
+ * name that folded onto an exact-prefix `mcp__` category (which Command Code
43446
+ * matches case-sensitively, unlike the name rule it came from).
43447
+ */
43448
+ function isFoldedMcpToolNameRule({ rule, category }) {
43449
+ const { tool } = splitCommandcodeRule(rule);
43450
+ return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
43451
+ }
43452
+ /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
43453
+ function scopedMcpRuleTool(rule) {
43454
+ const { tool, inner } = splitCommandcodeRule(rule);
43455
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
43456
+ }
43457
+ /**
43458
+ * The string entries of a Command Code permission list. Command Code reads
43459
+ * each entry on its own and skips one that is not a string, so a stray
43460
+ * `null` beside `Shell(rm -rf *)` must not throw the deny away with it; the
43461
+ * skipped entries are counted in a warning because they cannot be kept.
43462
+ */
43463
+ function stringRules({ list, action, outcome, warn }) {
43464
+ if (!Array.isArray(list)) return [];
43465
+ const rules = list.filter((entry) => typeof entry === "string");
43466
+ const skipped = list.length - rules.length;
43467
+ 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}.`);
43468
+ return rules;
43469
+ }
43470
+ /**
43471
+ * The canonical categories this run rebuilds — every category the canonical
43472
+ * config names (an empty `bash: {}` still reclaims the previous `Shell(...)`
43473
+ * entries, as in the Claude Code adapter) plus every rule it emits, keyed the
43474
+ * way an existing entry parses back (so `mcp: { github: "deny" }`, written as
43475
+ * `mcp__github`, claims the `mcp__github` entries of the previous run; case
43476
+ * variants of the friendly tool names fold together, while MCP names are
43477
+ * matched exactly, as Command Code does). An existing entry whose tool folds onto one of them is
43478
+ * rulesync's to replace, whatever list it sits in — otherwise flipping a rule
43479
+ * from deny to allow would leave the old deny behind and win. Every other
43480
+ * entry — an internal tool name rulesync cannot model, or a modeled tool the
43481
+ * canonical config does not mention — is the user's (Command Code writes
43482
+ * interactive approvals into the same lists) and is preserved verbatim.
43483
+ * A `deny`/`ask` entry that is reclaimed without an equally strict rule
43484
+ * taking its place (say a hand-written `mcp__*` deny next to a canonical
43485
+ * `mcp: { github: "deny" }`) is reported, so regenerating never silently
43486
+ * loosens what the user wrote. Mirrors `managedClaudeToolNames` in the
43487
+ * Claude Code adapter.
43488
+ */
43489
+ function preservedRules({ existingPermissions, key, managedCategories, written, logger }) {
43490
+ return stringRules({
43491
+ list: existingPermissions[key],
43492
+ action: key,
43493
+ outcome: "dropped from the regenerated list",
43494
+ warn: (message) => logger?.warn(message)
43495
+ }).filter((rule) => {
43496
+ const parsed = parseCommandcodeRule(rule);
43497
+ if (parsed === null || !managedCategories.has(parsed.category)) return true;
43498
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
43499
+ if (key !== "allow" && isStricterAction({
43500
+ action: key,
43501
+ existing: replacement
43502
+ })) 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.`);
43503
+ return false;
43504
+ });
43505
+ }
43506
+ /**
43507
+ * The rule to write for a canonical rule, or `null` when Command Code would
43508
+ * ignore it in the `action` list. A server-less wildcard (`*`, `mcp__*`) in
43509
+ * `allow` is warned about and left unwritten rather than written dead. A
43510
+ * specifier on an MCP rule is ignored by Command Code in every list, so a
43511
+ * scoped `deny`/`ask` is written as the bare tool (which is what Command
43512
+ * Code would enforce anyway, only now visibly) with a warning, and a scoped
43513
+ * `allow` — which would grant the whole tool — is refused. Judged on the
43514
+ * rule as written, so the bare `mcp` category cannot smuggle a specifier in
43515
+ * through a `<server>__<tool>(specifier)` pattern.
43516
+ */
43517
+ function writableCommandcodeRule({ rule, emitted, action, logger }) {
43518
+ if (action === "allow" && emitted.notInAllow) {
43519
+ 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.`);
43520
+ return null;
43521
+ }
43522
+ const tool = scopedMcpRuleTool(rule);
43523
+ if (tool === null) return rule;
43524
+ if (action === "allow") {
43525
+ 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.`);
43526
+ return null;
43527
+ }
43528
+ logger?.warn(`Command Code ignores the specifier of an MCP rule, so '${rule}' was written as '${tool}' and applies "${action}" to the whole tool.`);
43529
+ return tool;
43530
+ }
43531
+ /**
43532
+ * Record `action` for `rule`, keeping the stricter one (deny > ask > allow)
43533
+ * when two canonical rules collapse onto the same Command Code entry.
43534
+ */
43535
+ function rankCommandcodeRule({ ranked, rule, action, logger }) {
43536
+ const existing = ranked.get(rule);
43537
+ 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).`);
43538
+ if (isStricterAction({
43539
+ action,
43540
+ existing
43541
+ })) ranked.set(rule, action);
43542
+ }
43543
+ /**
43544
+ * Bucket the canonical rules into Command Code's `allow`/`ask`/`deny` lists.
43545
+ * Collisions resolve to the strictest action (deny > ask > allow); categories
43546
+ * Command Code cannot express are skipped with a warning when they carry a
43547
+ * `deny`; rules Command Code would ignore in the target list are dropped or
43548
+ * rewritten by `writableCommandcodeRule`.
43549
+ */
43550
+ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
43551
+ const permission = honorAllToolsOnBash(config.permission);
43552
+ const ranked = /* @__PURE__ */ new Map();
43553
+ const managedCategories = /* @__PURE__ */ new Set();
43554
+ for (const [category, rules] of Object.entries(permission)) {
43555
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
43556
+ const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
43557
+ if (managedCategory !== null) managedCategories.add(managedCategory.category);
43558
+ for (const [pattern, action] of Object.entries(rules)) {
43559
+ const rule = buildCommandcodeRule(category, pattern);
43560
+ const emitted = rule === null ? null : parseCommandcodeRule(rule);
43561
+ if (rule === null || emitted === null) {
43562
+ const honoredOnShell = category === "*" && permission.bash !== void 0;
43563
+ 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.`);
43564
+ continue;
43565
+ }
43566
+ managedCategories.add(emitted.category);
43567
+ const written = writableCommandcodeRule({
43568
+ rule,
43569
+ emitted,
43570
+ action,
43571
+ logger
43572
+ });
43573
+ if (written !== null) rankCommandcodeRule({
43574
+ ranked,
43575
+ rule: written,
43576
+ action,
43577
+ logger
43578
+ });
43579
+ }
43580
+ }
43581
+ const written = /* @__PURE__ */ new Map();
43582
+ for (const [rule, action] of ranked) {
43583
+ const parsed = parseCommandcodeRule(rule);
43584
+ if (parsed !== null) written.set(`${parsed.category}(${parsed.pattern})`, action);
43585
+ }
43586
+ const preserved = {
43587
+ existingPermissions,
43588
+ managedCategories,
43589
+ written,
43590
+ logger
43591
+ };
43592
+ const allow = preservedRules({
43593
+ ...preserved,
43594
+ key: "allow"
43595
+ });
43596
+ const ask = preservedRules({
43597
+ ...preserved,
43598
+ key: "ask"
43599
+ });
43600
+ const deny = preservedRules({
43601
+ ...preserved,
43602
+ key: "deny"
43603
+ });
43604
+ for (const [rule, action] of ranked) if (action === "allow") allow.push(rule);
43605
+ else if (action === "ask") ask.push(rule);
43606
+ else deny.push(rule);
43607
+ return {
43608
+ allow: (0, es_toolkit.uniq)(allow.toSorted()),
43609
+ ask: (0, es_toolkit.uniq)(ask.toSorted()),
43610
+ deny: (0, es_toolkit.uniq)(deny.toSorted())
43611
+ };
43612
+ }
43613
+ /**
43614
+ * Why a rule marked `notInAllow` is not imported from `allow`: Command Code
43615
+ * ignores a server-less wildcard or a tool-name glob there, while it does
43616
+ * enforce a scoped `MCP__<server>__<tool>(specifier)` — as a glob over the
43617
+ * call's arguments — and a padded `Shell( * )` — as a pattern narrower than
43618
+ * the bare `Shell` — which rulesync cannot model in `allow` without widening
43619
+ * the grant.
43620
+ */
43621
+ function skippedAllowImportMessage({ rule, category }) {
43622
+ const { tool, inner } = splitCommandcodeRule(rule);
43623
+ 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.`;
43624
+ 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.`;
43625
+ 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.`;
43626
+ }
43627
+ /**
43628
+ * Parse Command Code's `permissions` lists back into a canonical permission
43629
+ * map with `deny > ask > allow` precedence, so a tool described more than
43630
+ * once resolves to the strictest action. Each rule is imported as what
43631
+ * Command Code enforces for it, never as what it looks like: a rule Command
43632
+ * Code ignores in `allow` (`*`, `mcp__*`, an `MCP__...` glob) is skipped
43633
+ * there with a warning, since importing it would turn a dead line into a
43634
+ * live grant for every other target, and so is a scoped
43635
+ * `MCP__<server>__<tool>(specifier)` allow, which Command Code enforces as a
43636
+ * glob over the call's arguments but rulesync cannot import without widening
43637
+ * it to the whole tool; the specifier of an exact-prefix `mcp__...` rule,
43638
+ * which Command Code drops, is folded to the whole tool in every list — with
43639
+ * a warning in `allow`, where the fold widens the grant the user wrote. A
43640
+ * tool-half glob in that exact-prefix spelling (`mcp__<server>__get_*`), which
43641
+ * Command Code matches against tool names in every list, is imported as the
43642
+ * category spelled that way, the same one the generator writes back; other
43643
+ * targets read it as a literal tool name.
43644
+ */
43645
+ function parseCommandcodeRuleLists(permissions) {
43646
+ const permission = {};
43647
+ const lists = [
43648
+ ["allow", permissions.allow],
43649
+ ["ask", permissions.ask],
43650
+ ["deny", permissions.deny]
43651
+ ];
43652
+ for (const [action, rawList] of lists) {
43653
+ const list = stringRules({
43654
+ list: rawList,
43655
+ action,
43656
+ outcome: "not imported",
43657
+ warn: (message) => fallbackLogger.warn(message)
43658
+ });
43659
+ for (const rule of list) {
43660
+ const parsed = parseCommandcodeRule(rule);
43661
+ if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) {
43662
+ 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.`);
43663
+ continue;
43664
+ }
43665
+ const { category, pattern } = parsed;
43666
+ if (action === "allow" && parsed.notInAllow) {
43667
+ fallbackLogger.warn(skippedAllowImportMessage({
43668
+ rule,
43669
+ category
43670
+ }));
43671
+ continue;
43672
+ }
43673
+ 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.`);
43674
+ if (isFoldedMcpToolNameRule({
43675
+ rule,
43676
+ category
43677
+ })) 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.`);
43678
+ const rules = permission[category] ??= {};
43679
+ if (isStricterAction({
43680
+ action,
43681
+ existing: rules[pattern]
43682
+ })) rules[pattern] = action;
43683
+ }
43684
+ }
43685
+ return permission;
43686
+ }
43687
+ /**
43688
+ * Permissions generator for Command Code.
43689
+ *
43690
+ * Rules live under the `permissions` key of `.commandcode/settings.json`
43691
+ * (project) and `~/.commandcode/settings.json` (user) as three lists of
43692
+ * Claude-style entries: `deny` (always wins), `ask`, `allow`. The same key
43693
+ * also carries `defaultMode`, `additionalDirectories` and `disableBypass`,
43694
+ * and the file holds `hooks` and other settings, so writes go through the
43695
+ * shared-config gateway: rulesync replaces the three lists, keeps every
43696
+ * sibling key, and never deletes the file.
43697
+ *
43698
+ * Generate: `permission.<category>.<pattern>` becomes `Tool(pattern)` with
43699
+ * the friendly names (`Shell`, `Read`, `Edit`, `Write`, `WebFetch`,
43700
+ * `WebSearch`), `mcp__<server>__<tool>` names pass through, and the all-tools
43701
+ * `*` category becomes the bare `*` rule Command Code accepts in `deny`/`ask`.
43702
+ * Only the entries of the categories the canonical config names are rebuilt;
43703
+ * every other existing entry is preserved verbatim.
43704
+ * Import: the lists are parsed back as what Command Code enforces for each
43705
+ * entry — friendly tool names case-insensitively (with the internal aliases
43706
+ * such as `Bash`, `PowerShell` and `write_file` folding onto their
43707
+ * category), MCP names exactly; rules for internal tool names rulesync does
43708
+ * not model are skipped.
43709
+ *
43710
+ * @see https://commandcode.ai/docs/permissions
43711
+ * @see https://commandcode.ai/docs/settings
43712
+ */
43713
+ var CommandcodePermissions = class CommandcodePermissions extends ToolPermissions {
43714
+ constructor(params) {
43715
+ super({
43716
+ ...params,
43717
+ fileContent: params.fileContent ?? "{}"
43718
+ });
43719
+ }
43720
+ /**
43721
+ * `settings.json` holds hooks and other user settings, so it is never
43722
+ * deleted; clearing permissions happens via an in-place merge.
43723
+ */
43724
+ isDeletable() {
43725
+ return false;
43726
+ }
43727
+ static getSettablePaths(_options = {}) {
43728
+ return {
43729
+ relativeDirPath: COMMANDCODE_DIR,
43730
+ relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
43731
+ };
43732
+ }
43733
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
43734
+ const paths = CommandcodePermissions.getSettablePaths({ global });
43735
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
43736
+ return new CommandcodePermissions({
43737
+ outputRoot,
43738
+ relativeDirPath: paths.relativeDirPath,
43739
+ relativeFilePath: paths.relativeFilePath,
43740
+ fileContent,
43741
+ validate,
43742
+ global
43743
+ });
43744
+ }
43745
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
43746
+ const paths = CommandcodePermissions.getSettablePaths({ global });
43747
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
43748
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
43749
+ const existing = parseCommandcodeSettings({
43750
+ fileContent: existingContent,
43751
+ relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
43752
+ });
43753
+ const existingPermissions = isRecord$1(existing[COMMANDCODE_PERMISSIONS_KEY]) ? existing[COMMANDCODE_PERMISSIONS_KEY] : {};
43754
+ const lists = buildCommandcodeRuleLists({
43755
+ config: rulesyncPermissions.getJson(),
43756
+ existingPermissions,
43757
+ logger
43758
+ });
43759
+ const permissions = { ...existingPermissions };
43760
+ for (const key of [
43761
+ "allow",
43762
+ "ask",
43763
+ "deny"
43764
+ ]) if (lists[key].length > 0) permissions[key] = lists[key];
43765
+ else delete permissions[key];
43766
+ return new CommandcodePermissions({
43767
+ outputRoot,
43768
+ relativeDirPath: paths.relativeDirPath,
43769
+ relativeFilePath: paths.relativeFilePath,
43770
+ fileContent: applySharedConfigPatch({
43771
+ fileKey: sharedConfigFileKey(paths),
43772
+ feature: "permissions",
43773
+ existingContent,
43774
+ patch: { [COMMANDCODE_PERMISSIONS_KEY]: permissions },
43775
+ filePath
43776
+ }),
43777
+ validate: true,
43778
+ global
43779
+ });
43780
+ }
43781
+ toRulesyncPermissions() {
43782
+ const settings = parseCommandcodeSettings({
43783
+ fileContent: this.getFileContent() || "{}",
43784
+ relativePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
43785
+ });
43786
+ const permissions = isRecord$1(settings[COMMANDCODE_PERMISSIONS_KEY]) ? settings[COMMANDCODE_PERMISSIONS_KEY] : {};
43787
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission: parseCommandcodeRuleLists(permissions) }, null, 2) });
43788
+ }
43789
+ validate() {
43790
+ return {
43791
+ success: true,
43792
+ error: null
43793
+ };
43794
+ }
43795
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
43796
+ return new CommandcodePermissions({
43797
+ outputRoot,
43798
+ relativeDirPath,
43799
+ relativeFilePath,
43800
+ fileContent: "{}",
43801
+ validate: false,
43802
+ global
43803
+ });
43804
+ }
43805
+ };
43806
+ /**
43807
+ * Fail closed on a syntax error or non-object root, matching the write path's
43808
+ * shared-config declaration, so a broken file is surfaced rather than
43809
+ * partially imported or overwritten.
43810
+ */
43811
+ function parseCommandcodeSettings({ fileContent, relativePath }) {
43812
+ try {
43813
+ return parseSharedConfig({
43814
+ format: "json",
43815
+ fileContent,
43816
+ filePath: relativePath,
43817
+ invalidRootPolicy: "error"
43818
+ });
43819
+ } catch (error) {
43820
+ throw new Error(`Failed to parse Command Code settings in ${relativePath}: ${formatError(error)}`, { cause: error });
43821
+ }
43822
+ }
43823
+ //#endregion
42611
43824
  //#region src/features/permissions/continue-permissions.ts
42612
43825
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
42613
43826
  const CATCH_ALL_PATTERN$5 = "*";
@@ -53369,6 +54582,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
53369
54582
  supportsImport: true
53370
54583
  }
53371
54584
  }],
54585
+ ["commandcode", {
54586
+ class: CommandcodePermissions,
54587
+ meta: {
54588
+ supportsProject: true,
54589
+ supportsGlobal: true,
54590
+ supportsImport: true
54591
+ }
54592
+ }],
53372
54593
  ["continue", {
53373
54594
  class: ContinuePermissions,
53374
54595
  meta: {
@@ -56135,6 +57356,154 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
56135
57356
  }
56136
57357
  };
56137
57358
  //#endregion
57359
+ //#region src/features/skills/commandcode-skill.ts
57360
+ const CommandcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
57361
+ name: zod_mini.z.string(),
57362
+ description: zod_mini.z.string()
57363
+ });
57364
+ /**
57365
+ * Represents a Command Code skill directory.
57366
+ *
57367
+ * Command Code discovers Anthropic-style `<name>/SKILL.md` directories under
57368
+ * `<project>/.commandcode/skills/` (project scope) and
57369
+ * `~/.commandcode/skills/` (user scope). The frontmatter requires `name`
57370
+ * (matching the directory name) and `description`; supporting files next to
57371
+ * `SKILL.md` are carried along.
57372
+ *
57373
+ * @see https://commandcode.ai/docs/skills
57374
+ */
57375
+ var CommandcodeSkill = class CommandcodeSkill extends ToolSkill {
57376
+ constructor({ outputRoot = process.cwd(), relativeDirPath = COMMANDCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
57377
+ super({
57378
+ outputRoot,
57379
+ relativeDirPath,
57380
+ dirName,
57381
+ mainFile: {
57382
+ name: SKILL_FILE_NAME,
57383
+ body,
57384
+ frontmatter: { ...frontmatter }
57385
+ },
57386
+ otherFiles,
57387
+ global
57388
+ });
57389
+ if (validate) {
57390
+ const result = this.validate();
57391
+ if (!result.success) throw result.error;
57392
+ }
57393
+ }
57394
+ static getSettablePaths(_options = {}) {
57395
+ return { relativeDirPath: COMMANDCODE_SKILLS_DIR_PATH };
57396
+ }
57397
+ getFrontmatter() {
57398
+ return CommandcodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
57399
+ }
57400
+ getBody() {
57401
+ return this.mainFile?.body ?? "";
57402
+ }
57403
+ validate() {
57404
+ if (!this.mainFile) return {
57405
+ success: false,
57406
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
57407
+ };
57408
+ const result = CommandcodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
57409
+ if (!result.success) return {
57410
+ success: false,
57411
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
57412
+ };
57413
+ if (result.data.name !== this.getDirName()) return {
57414
+ success: false,
57415
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: frontmatter name (${result.data.name}) must match directory name (${this.getDirName()})`)
57416
+ };
57417
+ return {
57418
+ success: true,
57419
+ error: null
57420
+ };
57421
+ }
57422
+ toRulesyncSkill() {
57423
+ const { name, description, ...commandcodeSection } = this.getFrontmatter();
57424
+ const rulesyncFrontmatter = {
57425
+ name,
57426
+ description,
57427
+ targets: ["*"],
57428
+ ...Object.keys(commandcodeSection).length > 0 && { commandcode: commandcodeSection }
57429
+ };
57430
+ return new RulesyncSkill({
57431
+ outputRoot: this.outputRoot,
57432
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
57433
+ dirName: this.getDirName(),
57434
+ frontmatter: rulesyncFrontmatter,
57435
+ body: this.getBody(),
57436
+ otherFiles: this.getOtherFiles(),
57437
+ validate: true,
57438
+ global: this.global
57439
+ });
57440
+ }
57441
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
57442
+ const settablePaths = CommandcodeSkill.getSettablePaths({ global });
57443
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
57444
+ const { name: _sectionName, description: _sectionDescription, ...commandcodeSection } = rulesyncFrontmatter.commandcode ?? {};
57445
+ const commandcodeFrontmatter = {
57446
+ ...commandcodeSection,
57447
+ name: rulesyncFrontmatter.name,
57448
+ description: rulesyncFrontmatter.description
57449
+ };
57450
+ return new CommandcodeSkill({
57451
+ outputRoot,
57452
+ relativeDirPath: settablePaths.relativeDirPath,
57453
+ dirName: commandcodeFrontmatter.name,
57454
+ frontmatter: commandcodeFrontmatter,
57455
+ body: rulesyncSkill.getBody(),
57456
+ otherFiles: rulesyncSkill.getOtherFiles(),
57457
+ validate,
57458
+ global
57459
+ });
57460
+ }
57461
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
57462
+ const targets = rulesyncSkill.getFrontmatter().targets;
57463
+ return targets.includes("*") || targets.includes("commandcode");
57464
+ }
57465
+ static async fromDir(params) {
57466
+ const loaded = await this.loadSkillDirContent({
57467
+ ...params,
57468
+ getSettablePaths: CommandcodeSkill.getSettablePaths
57469
+ });
57470
+ const result = CommandcodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
57471
+ if (!result.success) {
57472
+ const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
57473
+ throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
57474
+ }
57475
+ if (result.data.name !== loaded.dirName) {
57476
+ const skillFilePath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName, SKILL_FILE_NAME);
57477
+ throw new Error(`Frontmatter name (${result.data.name}) must match directory name (${loaded.dirName}) in ${skillFilePath}`);
57478
+ }
57479
+ return new CommandcodeSkill({
57480
+ outputRoot: loaded.outputRoot,
57481
+ relativeDirPath: loaded.relativeDirPath,
57482
+ dirName: loaded.dirName,
57483
+ frontmatter: result.data,
57484
+ body: loaded.body,
57485
+ otherFiles: loaded.otherFiles,
57486
+ validate: true,
57487
+ global: loaded.global
57488
+ });
57489
+ }
57490
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
57491
+ return new CommandcodeSkill({
57492
+ outputRoot,
57493
+ relativeDirPath,
57494
+ dirName,
57495
+ frontmatter: {
57496
+ name: "",
57497
+ description: ""
57498
+ },
57499
+ body: "",
57500
+ otherFiles: [],
57501
+ validate: false,
57502
+ global
57503
+ });
57504
+ }
57505
+ };
57506
+ //#endregion
56138
57507
  //#region src/features/skills/continue-skill.ts
56139
57508
  const ContinueSkillFrontmatterSchema = zod_mini.z.looseObject({
56140
57509
  name: zod_mini.z.string(),
@@ -61324,6 +62693,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
61324
62693
  supportsGlobal: true
61325
62694
  }
61326
62695
  }],
62696
+ ["commandcode", {
62697
+ class: CommandcodeSkill,
62698
+ meta: {
62699
+ supportsProject: true,
62700
+ supportsSimulated: false,
62701
+ supportsGlobal: true
62702
+ }
62703
+ }],
61327
62704
  ["continue", {
61328
62705
  class: ContinueSkill,
61329
62706
  meta: {
@@ -63427,6 +64804,143 @@ var CodexCliSubagent = class CodexCliSubagent extends ToolSubagent {
63427
64804
  }
63428
64805
  };
63429
64806
  //#endregion
64807
+ //#region src/features/subagents/commandcode-subagent.ts
64808
+ const CommandcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
64809
+ name: zod_mini.z.string(),
64810
+ description: zod_mini.z.optional(zod_mini.z.string()),
64811
+ tools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
64812
+ disallowedTools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
64813
+ model: zod_mini.z.optional(zod_mini.z.string()),
64814
+ reasoningEffort: zod_mini.z.optional(zod_mini.z.string()),
64815
+ maxTurns: zod_mini.z.optional(zod_mini.z.number()),
64816
+ permissionMode: zod_mini.z.optional(zod_mini.z.string()),
64817
+ background: zod_mini.z.optional(zod_mini.z.boolean()),
64818
+ showOutput: zod_mini.z.optional(zod_mini.z.boolean())
64819
+ });
64820
+ /**
64821
+ * Command Code custom agent: a Markdown file with YAML frontmatter under
64822
+ * `.commandcode/agents/` (project) or `~/.commandcode/agents/` (user); the
64823
+ * body is the agent's system prompt.
64824
+ *
64825
+ * @see https://commandcode.ai/docs/custom-agents
64826
+ */
64827
+ var CommandcodeSubagent = class CommandcodeSubagent extends ToolSubagent {
64828
+ frontmatter;
64829
+ body;
64830
+ constructor({ frontmatter, body, fileContent, ...rest }) {
64831
+ if (rest.validate !== false) {
64832
+ const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
64833
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
64834
+ }
64835
+ super({
64836
+ ...rest,
64837
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
64838
+ });
64839
+ this.frontmatter = frontmatter;
64840
+ this.body = body;
64841
+ }
64842
+ static getSettablePaths(_options = {}) {
64843
+ return { relativeDirPath: COMMANDCODE_AGENTS_DIR_PATH };
64844
+ }
64845
+ getFrontmatter() {
64846
+ return this.frontmatter;
64847
+ }
64848
+ getBody() {
64849
+ return this.body;
64850
+ }
64851
+ toRulesyncSubagent() {
64852
+ const { name, description, ...rest } = this.frontmatter;
64853
+ return new RulesyncSubagent({
64854
+ outputRoot: ".",
64855
+ frontmatter: {
64856
+ targets: ["*"],
64857
+ name,
64858
+ description,
64859
+ commandcode: { ...rest }
64860
+ },
64861
+ body: this.body,
64862
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
64863
+ relativeFilePath: this.getRelativeFilePath(),
64864
+ validate: true
64865
+ });
64866
+ }
64867
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
64868
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
64869
+ const commandcodeSection = rulesyncFrontmatter.commandcode ?? {};
64870
+ const commandcodeSubagentFrontmatter = {
64871
+ name: rulesyncFrontmatter.name,
64872
+ description: rulesyncFrontmatter.description,
64873
+ ...commandcodeSection
64874
+ };
64875
+ const body = rulesyncSubagent.getBody();
64876
+ const fileContent = stringifyFrontmatter(body, commandcodeSubagentFrontmatter, { avoidBlockScalars: true });
64877
+ const paths = this.getSettablePaths({ global });
64878
+ return new CommandcodeSubagent({
64879
+ outputRoot,
64880
+ frontmatter: commandcodeSubagentFrontmatter,
64881
+ body,
64882
+ relativeDirPath: paths.relativeDirPath,
64883
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
64884
+ fileContent,
64885
+ validate,
64886
+ global
64887
+ });
64888
+ }
64889
+ validate() {
64890
+ if (!this.frontmatter) return {
64891
+ success: true,
64892
+ error: null
64893
+ };
64894
+ const result = CommandcodeSubagentFrontmatterSchema.safeParse(this.frontmatter);
64895
+ if (result.success) return {
64896
+ success: true,
64897
+ error: null
64898
+ };
64899
+ else return {
64900
+ success: false,
64901
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
64902
+ };
64903
+ }
64904
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
64905
+ return this.isTargetedByRulesyncSubagentDefault({
64906
+ rulesyncSubagent,
64907
+ toolTarget: "commandcode"
64908
+ });
64909
+ }
64910
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
64911
+ const paths = this.getSettablePaths({ global });
64912
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
64913
+ const fileContent = await readFileContent(filePath);
64914
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
64915
+ const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
64916
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
64917
+ return new CommandcodeSubagent({
64918
+ outputRoot,
64919
+ relativeDirPath: paths.relativeDirPath,
64920
+ relativeFilePath,
64921
+ frontmatter: result.data,
64922
+ body: content.trim(),
64923
+ fileContent,
64924
+ validate,
64925
+ global
64926
+ });
64927
+ }
64928
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
64929
+ return new CommandcodeSubagent({
64930
+ outputRoot,
64931
+ relativeDirPath,
64932
+ relativeFilePath,
64933
+ frontmatter: {
64934
+ name: "",
64935
+ description: ""
64936
+ },
64937
+ body: "",
64938
+ fileContent: "",
64939
+ validate: false
64940
+ });
64941
+ }
64942
+ };
64943
+ //#endregion
63430
64944
  //#region src/features/subagents/copilot-subagent.ts
63431
64945
  const CopilotSubagentFrontmatterSchema = zod_mini.z.looseObject({
63432
64946
  name: zod_mini.z.string(),
@@ -67085,6 +68599,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
67085
68599
  filePattern: "*.toml"
67086
68600
  }
67087
68601
  }],
68602
+ ["commandcode", {
68603
+ class: CommandcodeSubagent,
68604
+ meta: {
68605
+ supportsProject: true,
68606
+ supportsSimulated: false,
68607
+ supportsGlobal: true,
68608
+ filePattern: "*.md"
68609
+ }
68610
+ }],
67088
68611
  ["copilot", {
67089
68612
  class: CopilotSubagent,
67090
68613
  meta: {
@@ -69917,6 +71440,73 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
69917
71440
  }
69918
71441
  };
69919
71442
  //#endregion
71443
+ //#region src/features/rules/commandcode-rule.ts
71444
+ var CommandcodeRule = class CommandcodeRule extends ToolRule {
71445
+ constructor({ fileContent, root, ...rest }) {
71446
+ super({
71447
+ ...rest,
71448
+ fileContent,
71449
+ root: root ?? false
71450
+ });
71451
+ }
71452
+ static getSettablePaths({ global = false } = {}) {
71453
+ return { root: {
71454
+ relativeDirPath: global ? COMMANDCODE_DIR : ".",
71455
+ relativeFilePath: COMMANDCODE_RULE_FILE_NAME
71456
+ } };
71457
+ }
71458
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
71459
+ const { root } = this.getSettablePaths({ global });
71460
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
71461
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
71462
+ return new CommandcodeRule({
71463
+ outputRoot,
71464
+ relativeDirPath: root.relativeDirPath,
71465
+ relativeFilePath: root.relativeFilePath,
71466
+ fileContent,
71467
+ validate,
71468
+ root: true
71469
+ });
71470
+ }
71471
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
71472
+ const { root } = this.getSettablePaths({ global });
71473
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
71474
+ return new CommandcodeRule({
71475
+ outputRoot,
71476
+ relativeDirPath: root.relativeDirPath,
71477
+ relativeFilePath: root.relativeFilePath,
71478
+ fileContent: rulesyncRule.getBody(),
71479
+ validate,
71480
+ root: isRoot
71481
+ });
71482
+ }
71483
+ toRulesyncRule() {
71484
+ return this.toRulesyncRuleDefault();
71485
+ }
71486
+ validate() {
71487
+ return {
71488
+ success: true,
71489
+ error: null
71490
+ };
71491
+ }
71492
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
71493
+ return new CommandcodeRule({
71494
+ outputRoot,
71495
+ relativeDirPath,
71496
+ relativeFilePath,
71497
+ fileContent: "",
71498
+ validate: false,
71499
+ root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".commandcode")
71500
+ });
71501
+ }
71502
+ static isTargetedByRulesyncRule(rulesyncRule) {
71503
+ return this.isTargetedByRulesyncRuleDefault({
71504
+ rulesyncRule,
71505
+ toolTarget: "commandcode"
71506
+ });
71507
+ }
71508
+ };
71509
+ //#endregion
69920
71510
  //#region src/features/rules/continue-rule.ts
69921
71511
  /**
69922
71512
  * Frontmatter schema for Continue rule files (`.continue/rules/*.md`).
@@ -74237,6 +75827,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
74237
75827
  collisionPolicy: "fold"
74238
75828
  }
74239
75829
  }],
75830
+ ["commandcode", {
75831
+ class: CommandcodeRule,
75832
+ meta: {
75833
+ extension: "md",
75834
+ supportsGlobal: true,
75835
+ ruleDiscoveryMode: "auto",
75836
+ collisionPolicy: "fold"
75837
+ }
75838
+ }],
74240
75839
  ["continue", {
74241
75840
  class: ContinueRule,
74242
75841
  meta: {