rulesync 16.32.1 → 16.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli/index.cjs +4 -4
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-NHZnMc--.cjs → import-9T9fnjNn.cjs} +3361 -516
- package/dist/{import-CT7-pKL6.js → import-De13gNpF.js} +3346 -501
- package/dist/import-De13gNpF.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-CT7-pKL6.js.map +0 -1
|
@@ -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$
|
|
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$
|
|
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$
|
|
460
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
461
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
462
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
463
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
464
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
465
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
466
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
467
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
468
|
-
join$
|
|
469
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
470
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
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$
|
|
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$
|
|
476
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
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,10 +580,12 @@ const mcpProcessorToolTargetTuple = [
|
|
|
579
580
|
"claudecode-legacy",
|
|
580
581
|
"cline",
|
|
581
582
|
"codexcli",
|
|
583
|
+
"commandcode",
|
|
582
584
|
"continue",
|
|
583
585
|
"copilot",
|
|
584
586
|
"copilotcli",
|
|
585
587
|
"cortexcode",
|
|
588
|
+
"crush",
|
|
586
589
|
"cursor",
|
|
587
590
|
"deepagents",
|
|
588
591
|
"factorydroid",
|
|
@@ -621,6 +624,7 @@ const commandsProcessorToolTargetTuple = [
|
|
|
621
624
|
"claudecode-legacy",
|
|
622
625
|
"cline",
|
|
623
626
|
"codexcli",
|
|
627
|
+
"commandcode",
|
|
624
628
|
"continue",
|
|
625
629
|
"copilot",
|
|
626
630
|
"cursor",
|
|
@@ -658,6 +662,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
658
662
|
"claudecode-legacy",
|
|
659
663
|
"cline",
|
|
660
664
|
"codexcli",
|
|
665
|
+
"commandcode",
|
|
661
666
|
"copilot",
|
|
662
667
|
"copilotcli",
|
|
663
668
|
"cortexcode",
|
|
@@ -699,6 +704,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
699
704
|
"claudecode-legacy",
|
|
700
705
|
"cline",
|
|
701
706
|
"codexcli",
|
|
707
|
+
"commandcode",
|
|
702
708
|
"continue",
|
|
703
709
|
"copilot",
|
|
704
710
|
"copilotcli",
|
|
@@ -719,6 +725,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
719
725
|
"musecode",
|
|
720
726
|
"opencode",
|
|
721
727
|
"pi",
|
|
728
|
+
"pool",
|
|
722
729
|
"qwencode",
|
|
723
730
|
"reasonix",
|
|
724
731
|
"replit",
|
|
@@ -745,10 +752,12 @@ const hooksProcessorToolTargetTuple = [
|
|
|
745
752
|
"claudecode",
|
|
746
753
|
"claudecode-plugin",
|
|
747
754
|
"codexcli",
|
|
755
|
+
"commandcode",
|
|
748
756
|
"continue",
|
|
749
757
|
"copilot",
|
|
750
758
|
"copilotcli",
|
|
751
759
|
"cortexcode",
|
|
760
|
+
"crush",
|
|
752
761
|
"opencode",
|
|
753
762
|
"pi",
|
|
754
763
|
"factorydroid",
|
|
@@ -778,9 +787,11 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
778
787
|
"claudecode",
|
|
779
788
|
"cline",
|
|
780
789
|
"codexcli",
|
|
790
|
+
"commandcode",
|
|
781
791
|
"continue",
|
|
782
792
|
"copilot",
|
|
783
793
|
"copilotcli",
|
|
794
|
+
"crush",
|
|
784
795
|
"cursor",
|
|
785
796
|
"deepagents",
|
|
786
797
|
"devin",
|
|
@@ -3291,6 +3302,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3291
3302
|
".claude/settings.json",
|
|
3292
3303
|
".claude/settings.local.json",
|
|
3293
3304
|
".codex/config.toml",
|
|
3305
|
+
".commandcode/settings.json",
|
|
3294
3306
|
".continue/settings.json",
|
|
3295
3307
|
".copilot/settings.json",
|
|
3296
3308
|
".github/copilot/settings.json",
|
|
@@ -3308,6 +3320,9 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3308
3320
|
"reasonix.toml",
|
|
3309
3321
|
".vscode/settings.json",
|
|
3310
3322
|
".zed/settings.json",
|
|
3323
|
+
"crush.json",
|
|
3324
|
+
".crush.json",
|
|
3325
|
+
".config/crush/crush.json",
|
|
3311
3326
|
"kilo.json",
|
|
3312
3327
|
"kilo.jsonc",
|
|
3313
3328
|
"opencode.json",
|
|
@@ -3512,6 +3527,27 @@ function omitPrototypePollutionKeys(record) {
|
|
|
3512
3527
|
}
|
|
3513
3528
|
return sanitized;
|
|
3514
3529
|
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
|
|
3532
|
+
* reachable through the value (directly, or through an array) is copied with
|
|
3533
|
+
* its prototype-pollution keys dropped. Scalars and arrays of scalars come back
|
|
3534
|
+
* as they are.
|
|
3535
|
+
*
|
|
3536
|
+
* Use when passing an unknown, user-supplied value through unchanged — an MCP
|
|
3537
|
+
* server field a tool documents that rulesync does not model — so a
|
|
3538
|
+
* `__proto__` key nested anywhere inside it cannot ride into the generated
|
|
3539
|
+
* config, where the consuming tool may merge the object without the same care.
|
|
3540
|
+
*/
|
|
3541
|
+
function omitPrototypePollutionKeysDeep(value) {
|
|
3542
|
+
if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
|
|
3543
|
+
if (value === null || typeof value !== "object") return value;
|
|
3544
|
+
const sanitized = {};
|
|
3545
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3546
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3547
|
+
sanitized[key] = omitPrototypePollutionKeysDeep(entry);
|
|
3548
|
+
}
|
|
3549
|
+
return sanitized;
|
|
3550
|
+
}
|
|
3515
3551
|
//#endregion
|
|
3516
3552
|
//#region src/utils/type-guards.ts
|
|
3517
3553
|
/**
|
|
@@ -4579,6 +4615,14 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4579
4615
|
"stop"
|
|
4580
4616
|
];
|
|
4581
4617
|
/**
|
|
4618
|
+
* Hook events supported by Crush.
|
|
4619
|
+
*
|
|
4620
|
+
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
4621
|
+
* (Claude Code-compatible payload; `matcher` is a regex on the tool name).
|
|
4622
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
4623
|
+
*/
|
|
4624
|
+
const CRUSH_HOOK_EVENTS = ["preToolUse"];
|
|
4625
|
+
/**
|
|
4582
4626
|
* Hook events supported by JetBrains Junie CLI.
|
|
4583
4627
|
*
|
|
4584
4628
|
* Junie CLI exposes seven lifecycle events under the `"hooks"` key of
|
|
@@ -4916,6 +4960,30 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
|
|
|
4916
4960
|
};
|
|
4917
4961
|
const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
4918
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
|
+
/**
|
|
4919
4987
|
* Hook events supported by the Continue CLI (`cn`).
|
|
4920
4988
|
*
|
|
4921
4989
|
* Continue reads a Claude-Code-compatible `hooks` key from
|
|
@@ -5121,8 +5189,10 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
5121
5189
|
grokcli: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5122
5190
|
"kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5123
5191
|
zcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5192
|
+
crush: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5124
5193
|
bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5125
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) })),
|
|
5126
5196
|
continue: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5127
5197
|
tabnine: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5128
5198
|
qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -5618,6 +5688,13 @@ const VIBE_TO_CANONICAL_EVENT_NAMES = {
|
|
|
5618
5688
|
post_agent_turn: "stop"
|
|
5619
5689
|
};
|
|
5620
5690
|
/**
|
|
5691
|
+
* Canonical -> Crush event names. Crush spells its events the Claude Code
|
|
5692
|
+
* way (`PreToolUse`).
|
|
5693
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
5694
|
+
*/
|
|
5695
|
+
const CANONICAL_TO_CRUSH_EVENT_NAMES = { preToolUse: "PreToolUse" };
|
|
5696
|
+
const CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5697
|
+
/**
|
|
5621
5698
|
* Map canonical camelCase event names to Qwen Code PascalCase.
|
|
5622
5699
|
*
|
|
5623
5700
|
* Qwen Code reuses the same Claude-style PascalCase event names for the events
|
|
@@ -6057,6 +6134,11 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
6057
6134
|
kiroAutoBlock: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6058
6135
|
musecodeMode: zod_mini.z.optional(zod_mini.z.enum(["required", "optional"])),
|
|
6059
6136
|
rovodevEnableInstructions: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6137
|
+
crushOauth: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6138
|
+
crushOauthClientId: zod_mini.z.optional(zod_mini.z.string()),
|
|
6139
|
+
crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
|
|
6140
|
+
crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
|
|
6141
|
+
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6060
6142
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
6061
6143
|
/**
|
|
6062
6144
|
* The canonical per-server tool allowlist.
|
|
@@ -6130,9 +6212,12 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
6130
6212
|
claudecode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6131
6213
|
cline: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6132
6214
|
codexcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6215
|
+
commandcode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6133
6216
|
continue: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6134
6217
|
copilot: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6135
6218
|
copilotcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6219
|
+
cortexcode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6220
|
+
crush: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6136
6221
|
cursor: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6137
6222
|
deepagents: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6138
6223
|
devin: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -6149,7 +6234,6 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
6149
6234
|
reasonix: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6150
6235
|
roo: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6151
6236
|
rovodev: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6152
|
-
cortexcode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6153
6237
|
tabnine: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6154
6238
|
takt: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6155
6239
|
vibe: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -6481,6 +6565,15 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6481
6565
|
"musecodeMode",
|
|
6482
6566
|
"rovodevEnableInstructions",
|
|
6483
6567
|
"enable_instructions",
|
|
6568
|
+
"crushOauth",
|
|
6569
|
+
"crushOauthClientId",
|
|
6570
|
+
"crushOauthClientSecret",
|
|
6571
|
+
"crushOauthCallbackPort",
|
|
6572
|
+
"crushSessionless",
|
|
6573
|
+
"oauth_client_id",
|
|
6574
|
+
"oauth_client_secret",
|
|
6575
|
+
"oauth_callback_port",
|
|
6576
|
+
"sessionless",
|
|
6484
6577
|
"enabled"
|
|
6485
6578
|
])];
|
|
6486
6579
|
}));
|
|
@@ -6515,12 +6608,16 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6515
6608
|
* Targets that share one output file resolve identically so the shared
|
|
6516
6609
|
* file's content never depends on which of them generates last — see
|
|
6517
6610
|
* `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
|
|
6518
|
-
* and the
|
|
6611
|
+
* the Antigravity pair, and the project-scope Claude Code / Command Code
|
|
6612
|
+
* pair, which is why the scope is part of the resolution).
|
|
6519
6613
|
*
|
|
6520
6614
|
* Returns the same instance when neither mechanism is used.
|
|
6521
6615
|
*/
|
|
6522
|
-
forTarget({ toolTarget, logger }) {
|
|
6523
|
-
const { blockKeys, acceptedTargetNames } = resolveMcpTarget({
|
|
6616
|
+
forTarget({ toolTarget, global = false, logger }) {
|
|
6617
|
+
const { blockKeys, acceptedTargetNames } = resolveMcpTarget({
|
|
6618
|
+
toolTarget,
|
|
6619
|
+
global
|
|
6620
|
+
});
|
|
6524
6621
|
const json = this.json;
|
|
6525
6622
|
const sharedServers = this.json.mcpServers ?? {};
|
|
6526
6623
|
const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
|
|
@@ -6607,9 +6704,24 @@ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
|
|
|
6607
6704
|
* `config`) — so both targets always apply both blocks in a fixed order
|
|
6608
6705
|
* (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
|
|
6609
6706
|
* per server on conflict).
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
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 {
|
|
6613
6725
|
blockKeys: ["claudecode"],
|
|
6614
6726
|
acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
|
|
6615
6727
|
};
|
|
@@ -6929,7 +7041,7 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6929
7041
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
6930
7042
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
6931
7043
|
* `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
|
|
6932
|
-
* `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
7044
|
+
* `todoWrite`, `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
6933
7045
|
* `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
|
|
6934
7046
|
* `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
|
|
6935
7047
|
* `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
|
|
@@ -6942,7 +7054,7 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6942
7054
|
* changes what the file said is reported in either scope, naming what that key
|
|
6943
7055
|
* decides there — the autonomy and containment
|
|
6944
7056
|
* controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
|
|
6945
|
-
* registry controls (`disabled`, `visible`, `listDirectory`, and `eager`, which
|
|
7057
|
+
* registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
|
|
6946
7058
|
* demotes an omitted tool to deferred rather than removing it, replacing rather
|
|
6947
7059
|
* than merging with the list a higher scope set), the Auto Mode
|
|
6948
7060
|
* classifier config, and, because these groups are loose objects, any key
|
|
@@ -6983,6 +7095,14 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6983
7095
|
* `looseObject` (verbatim passthrough). Both project and global scope are
|
|
6984
7096
|
* supported.
|
|
6985
7097
|
*
|
|
7098
|
+
* Only `tools` and `general` are written; any other key is ignored with a
|
|
7099
|
+
* warning. Within them, the paths Tabnine CLI runs as a command or sends its
|
|
7100
|
+
* traffic to — `tools.discoveryCommand`, `tools.callCommand`,
|
|
7101
|
+
* `tools.shell.pager`, `tools.sandbox.command`, `general.tabnineHost` and
|
|
7102
|
+
* `general.preferredEditor` — are refused with a warning rather than written,
|
|
7103
|
+
* so a fetched permissions file cannot point the CLI at an executable or a
|
|
7104
|
+
* server of its choosing; set those by hand in `settings.json`.
|
|
7105
|
+
*
|
|
6986
7106
|
* @example
|
|
6987
7107
|
* { "tools": { "core": ["read_file", "run_shell_command(git)"] },
|
|
6988
7108
|
* "general": { "defaultApprovalMode": "plan" } }
|
|
@@ -7766,7 +7886,9 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
7766
7886
|
"antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7767
7887
|
continue: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7768
7888
|
copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7889
|
+
commandcode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7769
7890
|
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7891
|
+
crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7770
7892
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7771
7893
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7772
7894
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
@@ -9487,6 +9609,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
9487
9609
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9488
9610
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
9489
9611
|
})),
|
|
9612
|
+
commandcode: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9490
9613
|
cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9491
9614
|
continue: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9492
9615
|
tabnine: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
@@ -12792,6 +12915,26 @@ const ZCODE_USER_CONFIG_DECLARATION = {
|
|
|
12792
12915
|
}
|
|
12793
12916
|
};
|
|
12794
12917
|
/**
|
|
12918
|
+
* Crush's JSON config, shared by the project (`crush.json` / `.crush.json`)
|
|
12919
|
+
* and global (`~/.config/crush/crush.json`) spellings so a policy edit lands
|
|
12920
|
+
* on both. See the `SHARED_CONFIG_OWNERSHIP` entries for the key rationale.
|
|
12921
|
+
*/
|
|
12922
|
+
const CRUSH_CONFIG_DECLARATION = {
|
|
12923
|
+
format: "json",
|
|
12924
|
+
invalidRootPolicy: "error",
|
|
12925
|
+
features: {
|
|
12926
|
+
mcp: {
|
|
12927
|
+
kind: "replace-owned-keys",
|
|
12928
|
+
ownedKeys: ["mcp"]
|
|
12929
|
+
},
|
|
12930
|
+
hooks: {
|
|
12931
|
+
kind: "replace-owned-keys",
|
|
12932
|
+
ownedKeys: ["hooks"]
|
|
12933
|
+
},
|
|
12934
|
+
permissions: { kind: "deep-merge" }
|
|
12935
|
+
}
|
|
12936
|
+
};
|
|
12937
|
+
/**
|
|
12795
12938
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
12796
12939
|
* and both validate against the one published schema, which the gateway
|
|
12797
12940
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13052,6 +13195,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13052
13195
|
},
|
|
13053
13196
|
".zcode/config.json": ZCODE_WORKSPACE_CONFIG_DECLARATION,
|
|
13054
13197
|
".zcode/cli/config.json": ZCODE_USER_CONFIG_DECLARATION,
|
|
13198
|
+
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13199
|
+
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13055
13200
|
".bob/settings.json": {
|
|
13056
13201
|
format: "json",
|
|
13057
13202
|
invalidRootPolicy: "error",
|
|
@@ -13076,6 +13221,20 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13076
13221
|
ownedKeys: ["hooks"]
|
|
13077
13222
|
} }
|
|
13078
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
|
+
},
|
|
13079
13238
|
".cortex/settings.json": {
|
|
13080
13239
|
format: "json",
|
|
13081
13240
|
invalidRootPolicy: "error",
|
|
@@ -15277,6 +15436,94 @@ var CodexcliCommand = class CodexcliCommand extends ToolCommand {
|
|
|
15277
15436
|
}
|
|
15278
15437
|
};
|
|
15279
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
|
|
15280
15527
|
//#region src/constants/continue-paths.ts
|
|
15281
15528
|
const CONTINUE_DIR = ".continue";
|
|
15282
15529
|
const CONTINUE_ROOT_RULE_FILE_NAME = "AGENTS.md";
|
|
@@ -18901,15 +19148,12 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
|
|
|
18901
19148
|
return this.body;
|
|
18902
19149
|
}
|
|
18903
19150
|
getFrontmatter() {
|
|
18904
|
-
return
|
|
18905
|
-
description: this.frontmatter.description,
|
|
18906
|
-
prompt: this.frontmatter.prompt
|
|
18907
|
-
};
|
|
19151
|
+
return this.frontmatter;
|
|
18908
19152
|
}
|
|
18909
19153
|
toRulesyncCommand() {
|
|
18910
19154
|
const { description, prompt: _prompt, ...restFields } = this.frontmatter;
|
|
18911
19155
|
const rulesyncFrontmatter = {
|
|
18912
|
-
targets: ["
|
|
19156
|
+
targets: ["*"],
|
|
18913
19157
|
description,
|
|
18914
19158
|
...Object.keys(restFields).length > 0 && { tabnine: restFields }
|
|
18915
19159
|
};
|
|
@@ -19523,6 +19767,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
19523
19767
|
supportsSubdirectory: false
|
|
19524
19768
|
}
|
|
19525
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
|
+
}],
|
|
19526
19780
|
["continue", {
|
|
19527
19781
|
class: ContinueCommand,
|
|
19528
19782
|
meta: {
|
|
@@ -20872,7 +21126,20 @@ function stableJson(value) {
|
|
|
20872
21126
|
*/
|
|
20873
21127
|
function isSafeStringRecord(value) {
|
|
20874
21128
|
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
20875
|
-
return Object.entries(value).every(([key, entry]) =>
|
|
21129
|
+
return Object.entries(value).every(([key, entry]) => isSafeEnvEntry({
|
|
21130
|
+
key,
|
|
21131
|
+
value: entry
|
|
21132
|
+
}));
|
|
21133
|
+
}
|
|
21134
|
+
/**
|
|
21135
|
+
* Whether one `env` entry survives the rule {@link isSafeStringRecord} applies
|
|
21136
|
+
* to the whole map. Exported for the adapters that hand a tool an environment
|
|
21137
|
+
* block outside the shared converter (Tabnine, whose hook shape it does not
|
|
21138
|
+
* express) so they drop the same entries this one does.
|
|
21139
|
+
*/
|
|
21140
|
+
function isSafeEnvEntry(entry) {
|
|
21141
|
+
const { key, value } = entry;
|
|
21142
|
+
return typeof value === "string" && key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || value.includes(char));
|
|
20876
21143
|
}
|
|
20877
21144
|
/**
|
|
20878
21145
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
@@ -21763,135 +22030,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
21763
22030
|
}
|
|
21764
22031
|
};
|
|
21765
22032
|
//#endregion
|
|
21766
|
-
//#region src/features/hooks/bob-hooks.ts
|
|
21767
|
-
const BOB_CONVERTER_CONFIG = {
|
|
21768
|
-
supportedEvents: BOB_HOOK_EVENTS,
|
|
21769
|
-
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
21770
|
-
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
21771
|
-
projectDirVar: "",
|
|
21772
|
-
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
21773
|
-
"sessionStart",
|
|
21774
|
-
"beforeSubmitPrompt",
|
|
21775
|
-
"stop"
|
|
21776
|
-
]),
|
|
21777
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
21778
|
-
wildcardMatcherMeansAll: true
|
|
21779
|
-
};
|
|
21780
|
-
/**
|
|
21781
|
-
* Single spelling of the settings.json codec/policy: fail closed on an
|
|
21782
|
-
* unparseable root rather than replacing the user's Bob settings with
|
|
21783
|
-
* generated output.
|
|
21784
|
-
*/
|
|
21785
|
-
function parseBobSettings(fileContent, filePath) {
|
|
21786
|
-
return parseSharedConfig({
|
|
21787
|
-
format: "json",
|
|
21788
|
-
fileContent,
|
|
21789
|
-
filePath,
|
|
21790
|
-
invalidRootPolicy: "error"
|
|
21791
|
-
});
|
|
21792
|
-
}
|
|
21793
|
-
/**
|
|
21794
|
-
* IBM Bob lifecycle hooks.
|
|
21795
|
-
*
|
|
21796
|
-
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
21797
|
-
* `<project>/.bob/settings.json` (project scope) and
|
|
21798
|
-
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
21799
|
-
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
21800
|
-
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
21801
|
-
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
21802
|
-
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
21803
|
-
*
|
|
21804
|
-
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
21805
|
-
*/
|
|
21806
|
-
var BobHooks = class BobHooks extends ToolHooks {
|
|
21807
|
-
constructor(params) {
|
|
21808
|
-
super({
|
|
21809
|
-
...params,
|
|
21810
|
-
fileContent: params.fileContent ?? "{}"
|
|
21811
|
-
});
|
|
21812
|
-
}
|
|
21813
|
-
isDeletable() {
|
|
21814
|
-
return false;
|
|
21815
|
-
}
|
|
21816
|
-
static getSettablePaths({ global = false } = {}) {
|
|
21817
|
-
return {
|
|
21818
|
-
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
21819
|
-
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
21820
|
-
};
|
|
21821
|
-
}
|
|
21822
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
21823
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21824
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
21825
|
-
return new BobHooks({
|
|
21826
|
-
outputRoot,
|
|
21827
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21828
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21829
|
-
fileContent,
|
|
21830
|
-
validate
|
|
21831
|
-
});
|
|
21832
|
-
}
|
|
21833
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
21834
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21835
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21836
|
-
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
21837
|
-
const config = rulesyncHooks.getJson();
|
|
21838
|
-
const hooks = canonicalToToolHooks({
|
|
21839
|
-
config,
|
|
21840
|
-
toolOverrideHooks: config.bob?.hooks,
|
|
21841
|
-
converterConfig: BOB_CONVERTER_CONFIG,
|
|
21842
|
-
logger
|
|
21843
|
-
});
|
|
21844
|
-
const fileContent = applySharedConfigPatch({
|
|
21845
|
-
fileKey: sharedConfigFileKey(paths),
|
|
21846
|
-
feature: "hooks",
|
|
21847
|
-
existingContent,
|
|
21848
|
-
patch: { hooks },
|
|
21849
|
-
filePath,
|
|
21850
|
-
logger
|
|
21851
|
-
});
|
|
21852
|
-
return new BobHooks({
|
|
21853
|
-
outputRoot,
|
|
21854
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21855
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21856
|
-
fileContent,
|
|
21857
|
-
validate
|
|
21858
|
-
});
|
|
21859
|
-
}
|
|
21860
|
-
toRulesyncHooks({ logger } = {}) {
|
|
21861
|
-
const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
21862
|
-
let settings;
|
|
21863
|
-
try {
|
|
21864
|
-
settings = parseBobSettings(this.getFileContent(), configPath);
|
|
21865
|
-
} catch (error) {
|
|
21866
|
-
throw new Error(`Failed to parse Bob hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
21867
|
-
}
|
|
21868
|
-
const hooks = toolHooksToCanonical({
|
|
21869
|
-
logger,
|
|
21870
|
-
hooks: settings.hooks,
|
|
21871
|
-
converterConfig: BOB_CONVERTER_CONFIG
|
|
21872
|
-
});
|
|
21873
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
21874
|
-
hooks,
|
|
21875
|
-
overrideKey: "bob"
|
|
21876
|
-
}), null, 2) });
|
|
21877
|
-
}
|
|
21878
|
-
validate() {
|
|
21879
|
-
return {
|
|
21880
|
-
success: true,
|
|
21881
|
-
error: null
|
|
21882
|
-
};
|
|
21883
|
-
}
|
|
21884
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
21885
|
-
return new BobHooks({
|
|
21886
|
-
outputRoot,
|
|
21887
|
-
relativeDirPath,
|
|
21888
|
-
relativeFilePath,
|
|
21889
|
-
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
21890
|
-
validate: false
|
|
21891
|
-
});
|
|
21892
|
-
}
|
|
21893
|
-
};
|
|
21894
|
-
//#endregion
|
|
21895
22033
|
//#region src/features/hooks/preserve-unowned-hook-commands.ts
|
|
21896
22034
|
/**
|
|
21897
22035
|
* Read the `hooks` value from a destination JSON file.
|
|
@@ -22178,75 +22316,22 @@ function warnSkip({ logger, event, expected }) {
|
|
|
22178
22316
|
logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
|
|
22179
22317
|
}
|
|
22180
22318
|
//#endregion
|
|
22181
|
-
//#region src/features/hooks/
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
|
|
22185
|
-
|
|
22186
|
-
|
|
22187
|
-
|
|
22188
|
-
|
|
22189
|
-
|
|
22190
|
-
|
|
22191
|
-
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
"beforeSubmitPrompt",
|
|
22198
|
-
"stop"
|
|
22199
|
-
]),
|
|
22200
|
-
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22201
|
-
"command",
|
|
22202
|
-
"prompt",
|
|
22203
|
-
"http",
|
|
22204
|
-
"mcp_tool",
|
|
22205
|
-
"agent"
|
|
22206
|
-
]),
|
|
22207
|
-
emitsPromptModel: true,
|
|
22208
|
-
stringPassthroughFields: [
|
|
22209
|
-
{
|
|
22210
|
-
canonical: "if",
|
|
22211
|
-
tool: "if"
|
|
22212
|
-
},
|
|
22213
|
-
{
|
|
22214
|
-
canonical: "statusMessage",
|
|
22215
|
-
tool: "statusMessage"
|
|
22216
|
-
},
|
|
22217
|
-
{
|
|
22218
|
-
canonical: "shell",
|
|
22219
|
-
tool: "shell",
|
|
22220
|
-
commandOnly: true
|
|
22221
|
-
}
|
|
22222
|
-
],
|
|
22223
|
-
booleanPassthroughFields: [
|
|
22224
|
-
{
|
|
22225
|
-
canonical: "once",
|
|
22226
|
-
tool: "once"
|
|
22227
|
-
},
|
|
22228
|
-
{
|
|
22229
|
-
canonical: "async",
|
|
22230
|
-
tool: "async",
|
|
22231
|
-
commandOnly: true
|
|
22232
|
-
},
|
|
22233
|
-
{
|
|
22234
|
-
canonical: "asyncRewake",
|
|
22235
|
-
tool: "asyncRewake",
|
|
22236
|
-
commandOnly: true
|
|
22237
|
-
},
|
|
22238
|
-
{
|
|
22239
|
-
canonical: "continueOnBlock",
|
|
22240
|
-
tool: "continueOnBlock"
|
|
22241
|
-
}
|
|
22242
|
-
],
|
|
22243
|
-
arrayPassthroughFields: [{
|
|
22244
|
-
canonical: "args",
|
|
22245
|
-
tool: "args",
|
|
22246
|
-
commandOnly: true
|
|
22247
|
-
}]
|
|
22248
|
-
};
|
|
22249
|
-
var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
22319
|
+
//#region src/features/hooks/settings-json-hooks.ts
|
|
22320
|
+
/**
|
|
22321
|
+
* Shared implementation for the tools whose hooks live under the top-level
|
|
22322
|
+
* `hooks` key of a JSON settings file in Claude Code's shape —
|
|
22323
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command", ... }] }] }`
|
|
22324
|
+
* — and whose file also holds settings rulesync does not own, so generation
|
|
22325
|
+
* merges the `hooks` key into it (see `SHARED_CONFIG_OWNERSHIP`) instead of
|
|
22326
|
+
* overwriting it, and `--delete` never removes the file wholesale.
|
|
22327
|
+
*
|
|
22328
|
+
* A concrete adapter supplies its {@link SettingsJsonHooksSpec} and its
|
|
22329
|
+
* settable paths; the event mapping, the matcher rules and the per-hook
|
|
22330
|
+
* fields the tool documents are all expressed in the spec's converter config.
|
|
22331
|
+
* An adapter that also supports preserving unowned hooks (Claude Code) opts in
|
|
22332
|
+
* through {@link ToolHooks.supportsPreserveUnowned}.
|
|
22333
|
+
*/
|
|
22334
|
+
var SettingsJsonHooks = class extends ToolHooks {
|
|
22250
22335
|
constructor(params) {
|
|
22251
22336
|
super({
|
|
22252
22337
|
...params,
|
|
@@ -22256,24 +22341,42 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22256
22341
|
isDeletable() {
|
|
22257
22342
|
return false;
|
|
22258
22343
|
}
|
|
22344
|
+
/** The tool-specific half of the adapter. Every concrete class overrides this. */
|
|
22345
|
+
static getSpec() {
|
|
22346
|
+
throw new Error(`${this.name} does not define getSpec()`);
|
|
22347
|
+
}
|
|
22259
22348
|
/**
|
|
22260
|
-
* The converter config used for both directions.
|
|
22261
|
-
*
|
|
22262
|
-
*
|
|
22349
|
+
* The converter config used for both directions. A separate hook so a
|
|
22350
|
+
* subclass can swap tool-specific details (e.g. the project directory
|
|
22351
|
+
* variable) without redefining the whole spec.
|
|
22263
22352
|
*/
|
|
22264
22353
|
static getConverterConfig() {
|
|
22265
|
-
return
|
|
22354
|
+
return this.getSpec().converterConfig;
|
|
22266
22355
|
}
|
|
22267
22356
|
static getSettablePaths(_options = {}) {
|
|
22268
|
-
|
|
22269
|
-
|
|
22270
|
-
|
|
22271
|
-
|
|
22357
|
+
throw new Error(`${this.name} does not define getSettablePaths()`);
|
|
22358
|
+
}
|
|
22359
|
+
/**
|
|
22360
|
+
* The `SHARED_CONFIG_OWNERSHIP` key the settings file is merged under. By
|
|
22361
|
+
* default it is derived from the settable paths; an adapter whose file is
|
|
22362
|
+
* declared under another tool's key (the Claude Code plugin bundle) overrides
|
|
22363
|
+
* it.
|
|
22364
|
+
*/
|
|
22365
|
+
static getSharedFileKey(paths) {
|
|
22366
|
+
return sharedConfigFileKey(paths);
|
|
22367
|
+
}
|
|
22368
|
+
/**
|
|
22369
|
+
* `new this(params)` for the concrete adapter the static method was called
|
|
22370
|
+
* on; the cast is what an abstract class needs to be constructed through
|
|
22371
|
+
* `this`, and lives in one place.
|
|
22372
|
+
*/
|
|
22373
|
+
static instantiate(params) {
|
|
22374
|
+
return new this(params);
|
|
22272
22375
|
}
|
|
22273
22376
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22274
22377
|
const paths = this.getSettablePaths({ global });
|
|
22275
22378
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22276
|
-
return
|
|
22379
|
+
return this.instantiate({
|
|
22277
22380
|
outputRoot,
|
|
22278
22381
|
relativeDirPath: paths.relativeDirPath,
|
|
22279
22382
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22281,40 +22384,41 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22281
22384
|
validate
|
|
22282
22385
|
});
|
|
22283
22386
|
}
|
|
22284
|
-
static supportsPreserveUnowned() {
|
|
22285
|
-
return true;
|
|
22286
|
-
}
|
|
22287
22387
|
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
22288
|
-
return toolHooks instanceof
|
|
22388
|
+
return toolHooks instanceof this ? toolHooks.getOwnershipLockFiles() : [];
|
|
22289
22389
|
}
|
|
22290
22390
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
22291
22391
|
const paths = this.getSettablePaths({ global });
|
|
22292
22392
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22293
22393
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22294
22394
|
const config = rulesyncHooks.getJson();
|
|
22295
|
-
const
|
|
22395
|
+
const generatedHooks = canonicalToToolHooks({
|
|
22296
22396
|
config,
|
|
22297
|
-
toolOverrideHooks:
|
|
22397
|
+
toolOverrideHooks: overrideHooksOf({
|
|
22398
|
+
config,
|
|
22399
|
+
overrideKey: this.getSpec().overrideKey
|
|
22400
|
+
}),
|
|
22298
22401
|
converterConfig: this.getConverterConfig(),
|
|
22299
22402
|
logger
|
|
22300
22403
|
});
|
|
22301
22404
|
const preserving = preserveUnowned && this.supportsPreserveUnowned();
|
|
22302
22405
|
const merged = mergeGeneratedHookLists({
|
|
22303
22406
|
existingContent,
|
|
22304
|
-
generatedHooks
|
|
22407
|
+
generatedHooks,
|
|
22305
22408
|
shape: "matcher-groups",
|
|
22306
22409
|
preserveUnowned: preserving,
|
|
22307
22410
|
previouslyOwned: preserving ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
22308
22411
|
logger
|
|
22309
22412
|
});
|
|
22310
22413
|
const fileContent = applySharedConfigPatch({
|
|
22311
|
-
fileKey:
|
|
22414
|
+
fileKey: this.getSharedFileKey(paths),
|
|
22312
22415
|
feature: "hooks",
|
|
22313
22416
|
existingContent,
|
|
22314
22417
|
patch: { hooks: merged.hooks },
|
|
22315
|
-
filePath
|
|
22418
|
+
filePath,
|
|
22419
|
+
logger
|
|
22316
22420
|
});
|
|
22317
|
-
return
|
|
22421
|
+
return this.instantiate({
|
|
22318
22422
|
outputRoot,
|
|
22319
22423
|
relativeDirPath: paths.relativeDirPath,
|
|
22320
22424
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22324,20 +22428,29 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22324
22428
|
});
|
|
22325
22429
|
}
|
|
22326
22430
|
toRulesyncHooks({ logger } = {}) {
|
|
22431
|
+
const ctor = this.constructor;
|
|
22432
|
+
const spec = ctor.getSpec();
|
|
22433
|
+
const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
22327
22434
|
let settings;
|
|
22328
22435
|
try {
|
|
22329
|
-
settings =
|
|
22436
|
+
settings = parseSharedConfig({
|
|
22437
|
+
format: "json",
|
|
22438
|
+
fileContent: this.getFileContent(),
|
|
22439
|
+
filePath: configPath,
|
|
22440
|
+
invalidRootPolicy: "error"
|
|
22441
|
+
});
|
|
22330
22442
|
} catch (error) {
|
|
22331
|
-
|
|
22443
|
+
const reason = error instanceof Error && error.cause instanceof Error ? error.cause : error;
|
|
22444
|
+
throw new Error(`Failed to parse ${spec.displayName} hooks content in ${configPath}: ${formatError(reason)}`, { cause: error });
|
|
22332
22445
|
}
|
|
22333
22446
|
const hooks = toolHooksToCanonical({
|
|
22447
|
+
logger,
|
|
22334
22448
|
hooks: settings.hooks,
|
|
22335
|
-
converterConfig:
|
|
22336
|
-
logger
|
|
22449
|
+
converterConfig: ctor.getConverterConfig()
|
|
22337
22450
|
});
|
|
22338
22451
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
22339
22452
|
hooks,
|
|
22340
|
-
overrideKey:
|
|
22453
|
+
overrideKey: spec.overrideKey
|
|
22341
22454
|
}), null, 2) });
|
|
22342
22455
|
}
|
|
22343
22456
|
validate() {
|
|
@@ -22347,7 +22460,7 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22347
22460
|
};
|
|
22348
22461
|
}
|
|
22349
22462
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
22350
|
-
return
|
|
22463
|
+
return this.instantiate({
|
|
22351
22464
|
outputRoot,
|
|
22352
22465
|
relativeDirPath,
|
|
22353
22466
|
relativeFilePath,
|
|
@@ -22356,6 +22469,158 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22356
22469
|
});
|
|
22357
22470
|
}
|
|
22358
22471
|
};
|
|
22472
|
+
/**
|
|
22473
|
+
* The `hooks` map of a tool's override block (`config.<overrideKey>.hooks`),
|
|
22474
|
+
* or `undefined` when the block states none. The schema keeps every override
|
|
22475
|
+
* block loose, so the lookup goes through the record form.
|
|
22476
|
+
*/
|
|
22477
|
+
function overrideHooksOf({ config, overrideKey }) {
|
|
22478
|
+
const override = lookupOwn({
|
|
22479
|
+
record: config,
|
|
22480
|
+
key: overrideKey
|
|
22481
|
+
});
|
|
22482
|
+
if (!isPlainObject$1(override)) return;
|
|
22483
|
+
const hooks = override.hooks;
|
|
22484
|
+
return isPlainObject$1(hooks) ? hooks : void 0;
|
|
22485
|
+
}
|
|
22486
|
+
//#endregion
|
|
22487
|
+
//#region src/features/hooks/bob-hooks.ts
|
|
22488
|
+
const BOB_SPEC = {
|
|
22489
|
+
displayName: "Bob",
|
|
22490
|
+
overrideKey: "bob",
|
|
22491
|
+
converterConfig: {
|
|
22492
|
+
supportedEvents: BOB_HOOK_EVENTS,
|
|
22493
|
+
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
22494
|
+
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
22495
|
+
projectDirVar: "",
|
|
22496
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22497
|
+
"sessionStart",
|
|
22498
|
+
"beforeSubmitPrompt",
|
|
22499
|
+
"stop"
|
|
22500
|
+
]),
|
|
22501
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
22502
|
+
wildcardMatcherMeansAll: true
|
|
22503
|
+
}
|
|
22504
|
+
};
|
|
22505
|
+
/**
|
|
22506
|
+
* IBM Bob lifecycle hooks.
|
|
22507
|
+
*
|
|
22508
|
+
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
22509
|
+
* `<project>/.bob/settings.json` (project scope) and
|
|
22510
|
+
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
22511
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
22512
|
+
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
22513
|
+
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
22514
|
+
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
22515
|
+
*
|
|
22516
|
+
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
22517
|
+
*/
|
|
22518
|
+
var BobHooks = class extends SettingsJsonHooks {
|
|
22519
|
+
static getSpec() {
|
|
22520
|
+
return BOB_SPEC;
|
|
22521
|
+
}
|
|
22522
|
+
static getSettablePaths({ global = false } = {}) {
|
|
22523
|
+
return {
|
|
22524
|
+
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
22525
|
+
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
22526
|
+
};
|
|
22527
|
+
}
|
|
22528
|
+
};
|
|
22529
|
+
//#endregion
|
|
22530
|
+
//#region src/features/hooks/claudecode-hooks.ts
|
|
22531
|
+
const CLAUDE_SPEC = {
|
|
22532
|
+
displayName: "Claude",
|
|
22533
|
+
overrideKey: "claudecode",
|
|
22534
|
+
converterConfig: {
|
|
22535
|
+
supportedEvents: CLAUDE_HOOK_EVENTS,
|
|
22536
|
+
canonicalToToolEventNames: CANONICAL_TO_CLAUDE_EVENT_NAMES,
|
|
22537
|
+
toolToCanonicalEventNames: CLAUDE_TO_CANONICAL_EVENT_NAMES,
|
|
22538
|
+
projectDirVar: "$CLAUDE_PROJECT_DIR",
|
|
22539
|
+
prefixDotRelativeCommandsOnly: true,
|
|
22540
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22541
|
+
"worktreeCreate",
|
|
22542
|
+
"worktreeRemove",
|
|
22543
|
+
"messageDisplay",
|
|
22544
|
+
"postToolBatch",
|
|
22545
|
+
"taskCreated",
|
|
22546
|
+
"taskCompleted",
|
|
22547
|
+
"teammateIdle",
|
|
22548
|
+
"cwdChanged",
|
|
22549
|
+
"beforeSubmitPrompt",
|
|
22550
|
+
"stop"
|
|
22551
|
+
]),
|
|
22552
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22553
|
+
"command",
|
|
22554
|
+
"prompt",
|
|
22555
|
+
"http",
|
|
22556
|
+
"mcp_tool",
|
|
22557
|
+
"agent"
|
|
22558
|
+
]),
|
|
22559
|
+
emitsPromptModel: true,
|
|
22560
|
+
stringPassthroughFields: [
|
|
22561
|
+
{
|
|
22562
|
+
canonical: "if",
|
|
22563
|
+
tool: "if"
|
|
22564
|
+
},
|
|
22565
|
+
{
|
|
22566
|
+
canonical: "statusMessage",
|
|
22567
|
+
tool: "statusMessage"
|
|
22568
|
+
},
|
|
22569
|
+
{
|
|
22570
|
+
canonical: "shell",
|
|
22571
|
+
tool: "shell",
|
|
22572
|
+
commandOnly: true
|
|
22573
|
+
}
|
|
22574
|
+
],
|
|
22575
|
+
booleanPassthroughFields: [
|
|
22576
|
+
{
|
|
22577
|
+
canonical: "once",
|
|
22578
|
+
tool: "once"
|
|
22579
|
+
},
|
|
22580
|
+
{
|
|
22581
|
+
canonical: "async",
|
|
22582
|
+
tool: "async",
|
|
22583
|
+
commandOnly: true
|
|
22584
|
+
},
|
|
22585
|
+
{
|
|
22586
|
+
canonical: "asyncRewake",
|
|
22587
|
+
tool: "asyncRewake",
|
|
22588
|
+
commandOnly: true
|
|
22589
|
+
},
|
|
22590
|
+
{
|
|
22591
|
+
canonical: "continueOnBlock",
|
|
22592
|
+
tool: "continueOnBlock"
|
|
22593
|
+
}
|
|
22594
|
+
],
|
|
22595
|
+
arrayPassthroughFields: [{
|
|
22596
|
+
canonical: "args",
|
|
22597
|
+
tool: "args",
|
|
22598
|
+
commandOnly: true
|
|
22599
|
+
}]
|
|
22600
|
+
}
|
|
22601
|
+
};
|
|
22602
|
+
var ClaudecodeHooks = class extends SettingsJsonHooks {
|
|
22603
|
+
static getSpec() {
|
|
22604
|
+
return CLAUDE_SPEC;
|
|
22605
|
+
}
|
|
22606
|
+
static getSettablePaths(_options = {}) {
|
|
22607
|
+
return {
|
|
22608
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
22609
|
+
relativeFilePath: CLAUDECODE_SETTINGS_FILE_NAME
|
|
22610
|
+
};
|
|
22611
|
+
}
|
|
22612
|
+
/**
|
|
22613
|
+
* Every Claude-shaped settings file rulesync writes for Claude Code — the
|
|
22614
|
+
* project and user `settings.json` and the plugin bundle's `hooks.json` — is
|
|
22615
|
+
* declared under the one Claude key in `SHARED_CONFIG_OWNERSHIP`.
|
|
22616
|
+
*/
|
|
22617
|
+
static getSharedFileKey(_paths) {
|
|
22618
|
+
return CLAUDE_SETTINGS_SHARED_FILE_KEY;
|
|
22619
|
+
}
|
|
22620
|
+
static supportsPreserveUnowned() {
|
|
22621
|
+
return true;
|
|
22622
|
+
}
|
|
22623
|
+
};
|
|
22359
22624
|
//#endregion
|
|
22360
22625
|
//#region src/features/hooks/claudecode-plugin-hooks.ts
|
|
22361
22626
|
var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
@@ -22882,47 +23147,22 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
22882
23147
|
}
|
|
22883
23148
|
};
|
|
22884
23149
|
//#endregion
|
|
22885
|
-
//#region src/features/hooks/
|
|
22886
|
-
const
|
|
22887
|
-
supportedEvents:
|
|
22888
|
-
canonicalToToolEventNames:
|
|
22889
|
-
toolToCanonicalEventNames:
|
|
22890
|
-
projectDirVar: "$
|
|
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",
|
|
22891
23156
|
prefixDotRelativeCommandsOnly: true,
|
|
22892
|
-
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22893
|
-
|
|
22894
|
-
"stop",
|
|
22895
|
-
"teammateIdle",
|
|
22896
|
-
"taskCompleted",
|
|
22897
|
-
"worktreeCreate",
|
|
22898
|
-
"worktreeRemove"
|
|
22899
|
-
]),
|
|
22900
|
-
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22901
|
-
"command",
|
|
22902
|
-
"prompt",
|
|
22903
|
-
"http",
|
|
22904
|
-
"agent"
|
|
22905
|
-
]),
|
|
22906
|
-
emitsPromptModel: true,
|
|
22907
|
-
stringPassthroughFields: [{
|
|
22908
|
-
canonical: "statusMessage",
|
|
22909
|
-
tool: "statusMessage"
|
|
22910
|
-
}],
|
|
22911
|
-
booleanPassthroughFields: [{
|
|
22912
|
-
canonical: "once",
|
|
22913
|
-
tool: "once"
|
|
22914
|
-
}, {
|
|
22915
|
-
canonical: "async",
|
|
22916
|
-
tool: "async",
|
|
22917
|
-
commandOnly: true
|
|
22918
|
-
}]
|
|
23157
|
+
noMatcherEvents: /* @__PURE__ */ new Set(["stop", "sessionStart"]),
|
|
23158
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
22919
23159
|
};
|
|
22920
23160
|
/**
|
|
22921
|
-
* Single spelling of the settings
|
|
22922
|
-
*
|
|
22923
|
-
*
|
|
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.
|
|
22924
23164
|
*/
|
|
22925
|
-
function
|
|
23165
|
+
function parseCommandcodeSettings$1(fileContent, filePath) {
|
|
22926
23166
|
return parseSharedConfig({
|
|
22927
23167
|
format: "json",
|
|
22928
23168
|
fileContent,
|
|
@@ -22931,22 +23171,20 @@ function parseContinueSettings(fileContent, filePath) {
|
|
|
22931
23171
|
});
|
|
22932
23172
|
}
|
|
22933
23173
|
/**
|
|
22934
|
-
*
|
|
23174
|
+
* Command Code hooks.
|
|
22935
23175
|
*
|
|
22936
|
-
* Hooks live under the top-level `hooks` key of `<project>/.
|
|
22937
|
-
* (project scope) and `~/.
|
|
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
|
|
22938
23178
|
* Claude-Code shape: `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{
|
|
22939
|
-
* "type": "command"
|
|
22940
|
-
*
|
|
22941
|
-
* merges the `hooks` key into the existing file (see
|
|
22942
|
-
* instead of overwriting it.
|
|
22943
|
-
* `.continue/settings.local.json` and the Claude Code settings files; rulesync
|
|
22944
|
-
* writes only the committable project file and the user file.
|
|
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.
|
|
22945
23183
|
*
|
|
22946
|
-
* @see https://
|
|
22947
|
-
* @see https://
|
|
23184
|
+
* @see https://commandcode.ai/docs/hooks
|
|
23185
|
+
* @see https://commandcode.ai/docs/settings
|
|
22948
23186
|
*/
|
|
22949
|
-
var
|
|
23187
|
+
var CommandcodeHooks = class CommandcodeHooks extends ToolHooks {
|
|
22950
23188
|
constructor(params) {
|
|
22951
23189
|
super({
|
|
22952
23190
|
...params,
|
|
@@ -22958,14 +23196,14 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22958
23196
|
}
|
|
22959
23197
|
static getSettablePaths(_options = {}) {
|
|
22960
23198
|
return {
|
|
22961
|
-
relativeDirPath:
|
|
22962
|
-
relativeFilePath:
|
|
23199
|
+
relativeDirPath: COMMANDCODE_DIR,
|
|
23200
|
+
relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
|
|
22963
23201
|
};
|
|
22964
23202
|
}
|
|
22965
23203
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22966
|
-
const paths =
|
|
23204
|
+
const paths = CommandcodeHooks.getSettablePaths({ global });
|
|
22967
23205
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22968
|
-
return new
|
|
23206
|
+
return new CommandcodeHooks({
|
|
22969
23207
|
outputRoot,
|
|
22970
23208
|
relativeDirPath: paths.relativeDirPath,
|
|
22971
23209
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22974,14 +23212,14 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22974
23212
|
});
|
|
22975
23213
|
}
|
|
22976
23214
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
22977
|
-
const paths =
|
|
23215
|
+
const paths = CommandcodeHooks.getSettablePaths({ global });
|
|
22978
23216
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22979
23217
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22980
23218
|
const config = rulesyncHooks.getJson();
|
|
22981
23219
|
const hooks = canonicalToToolHooks({
|
|
22982
23220
|
config,
|
|
22983
|
-
toolOverrideHooks: config.
|
|
22984
|
-
converterConfig:
|
|
23221
|
+
toolOverrideHooks: config.commandcode?.hooks,
|
|
23222
|
+
converterConfig: COMMANDCODE_CONVERTER_CONFIG,
|
|
22985
23223
|
logger
|
|
22986
23224
|
});
|
|
22987
23225
|
const fileContent = applySharedConfigPatch({
|
|
@@ -22992,7 +23230,7 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22992
23230
|
filePath,
|
|
22993
23231
|
logger
|
|
22994
23232
|
});
|
|
22995
|
-
return new
|
|
23233
|
+
return new CommandcodeHooks({
|
|
22996
23234
|
outputRoot,
|
|
22997
23235
|
relativeDirPath: paths.relativeDirPath,
|
|
22998
23236
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -23004,18 +23242,18 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
23004
23242
|
const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
23005
23243
|
let settings;
|
|
23006
23244
|
try {
|
|
23007
|
-
settings =
|
|
23245
|
+
settings = parseCommandcodeSettings$1(this.getFileContent(), configPath);
|
|
23008
23246
|
} catch (error) {
|
|
23009
|
-
throw new Error(`Failed to parse
|
|
23247
|
+
throw new Error(`Failed to parse Command Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
23010
23248
|
}
|
|
23011
23249
|
const hooks = toolHooksToCanonical({
|
|
23012
23250
|
logger,
|
|
23013
23251
|
hooks: settings.hooks,
|
|
23014
|
-
converterConfig:
|
|
23252
|
+
converterConfig: COMMANDCODE_CONVERTER_CONFIG
|
|
23015
23253
|
});
|
|
23016
23254
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
23017
23255
|
hooks,
|
|
23018
|
-
overrideKey: "
|
|
23256
|
+
overrideKey: "commandcode"
|
|
23019
23257
|
}), null, 2) });
|
|
23020
23258
|
}
|
|
23021
23259
|
validate() {
|
|
@@ -23025,7 +23263,7 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
23025
23263
|
};
|
|
23026
23264
|
}
|
|
23027
23265
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23028
|
-
return new
|
|
23266
|
+
return new CommandcodeHooks({
|
|
23029
23267
|
outputRoot,
|
|
23030
23268
|
relativeDirPath,
|
|
23031
23269
|
relativeFilePath,
|
|
@@ -23035,6 +23273,73 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
23035
23273
|
}
|
|
23036
23274
|
};
|
|
23037
23275
|
//#endregion
|
|
23276
|
+
//#region src/features/hooks/continue-hooks.ts
|
|
23277
|
+
const CONTINUE_SPEC = {
|
|
23278
|
+
displayName: "Continue",
|
|
23279
|
+
overrideKey: "continue",
|
|
23280
|
+
converterConfig: {
|
|
23281
|
+
supportedEvents: CONTINUE_HOOK_EVENTS,
|
|
23282
|
+
canonicalToToolEventNames: CANONICAL_TO_CONTINUE_EVENT_NAMES,
|
|
23283
|
+
toolToCanonicalEventNames: CONTINUE_TO_CANONICAL_EVENT_NAMES,
|
|
23284
|
+
projectDirVar: "$CONTINUE_PROJECT_DIR",
|
|
23285
|
+
prefixDotRelativeCommandsOnly: true,
|
|
23286
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
23287
|
+
"beforeSubmitPrompt",
|
|
23288
|
+
"stop",
|
|
23289
|
+
"teammateIdle",
|
|
23290
|
+
"taskCompleted",
|
|
23291
|
+
"worktreeCreate",
|
|
23292
|
+
"worktreeRemove"
|
|
23293
|
+
]),
|
|
23294
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
23295
|
+
"command",
|
|
23296
|
+
"prompt",
|
|
23297
|
+
"http",
|
|
23298
|
+
"agent"
|
|
23299
|
+
]),
|
|
23300
|
+
emitsPromptModel: true,
|
|
23301
|
+
stringPassthroughFields: [{
|
|
23302
|
+
canonical: "statusMessage",
|
|
23303
|
+
tool: "statusMessage"
|
|
23304
|
+
}],
|
|
23305
|
+
booleanPassthroughFields: [{
|
|
23306
|
+
canonical: "once",
|
|
23307
|
+
tool: "once"
|
|
23308
|
+
}, {
|
|
23309
|
+
canonical: "async",
|
|
23310
|
+
tool: "async",
|
|
23311
|
+
commandOnly: true
|
|
23312
|
+
}]
|
|
23313
|
+
}
|
|
23314
|
+
};
|
|
23315
|
+
/**
|
|
23316
|
+
* Continue CLI hooks.
|
|
23317
|
+
*
|
|
23318
|
+
* Hooks live under the top-level `hooks` key of `<project>/.continue/settings.json`
|
|
23319
|
+
* (project scope) and `~/.continue/settings.json` (user scope), in the
|
|
23320
|
+
* Claude-Code shape: `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{
|
|
23321
|
+
* "type": "command" | "http" | "prompt" | "agent", ..., "timeout"?: <seconds>
|
|
23322
|
+
* }] }] }`. Both files also hold settings rulesync does not own, so generation
|
|
23323
|
+
* merges the `hooks` key into the existing file (see `SHARED_CONFIG_OWNERSHIP`)
|
|
23324
|
+
* instead of overwriting it. The CLI additionally reads
|
|
23325
|
+
* `.continue/settings.local.json` and the Claude Code settings files; rulesync
|
|
23326
|
+
* writes only the committable project file and the user file.
|
|
23327
|
+
*
|
|
23328
|
+
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/hookConfig.ts
|
|
23329
|
+
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/types.ts
|
|
23330
|
+
*/
|
|
23331
|
+
var ContinueHooks = class extends SettingsJsonHooks {
|
|
23332
|
+
static getSpec() {
|
|
23333
|
+
return CONTINUE_SPEC;
|
|
23334
|
+
}
|
|
23335
|
+
static getSettablePaths(_options = {}) {
|
|
23336
|
+
return {
|
|
23337
|
+
relativeDirPath: CONTINUE_DIR,
|
|
23338
|
+
relativeFilePath: CONTINUE_SETTINGS_FILE_NAME
|
|
23339
|
+
};
|
|
23340
|
+
}
|
|
23341
|
+
};
|
|
23342
|
+
//#endregion
|
|
23038
23343
|
//#region src/features/hooks/copilot-hooks.ts
|
|
23039
23344
|
/**
|
|
23040
23345
|
* Copilot hook entry as stored in .github/hooks/copilot-hooks.json.
|
|
@@ -23623,33 +23928,24 @@ const CORTEXCODE_SKILLS_DIR_PATH = (0, node_path.join)(CORTEXCODE_DIR, "skills")
|
|
|
23623
23928
|
const CORTEXCODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(CORTEXCODE_GLOBAL_DIR_PATH, "skills");
|
|
23624
23929
|
//#endregion
|
|
23625
23930
|
//#region src/features/hooks/cortexcode-hooks.ts
|
|
23626
|
-
const
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
|
|
23637
|
-
|
|
23931
|
+
const CORTEXCODE_SPEC = {
|
|
23932
|
+
displayName: "Cortex Code",
|
|
23933
|
+
overrideKey: "cortexcode",
|
|
23934
|
+
converterConfig: {
|
|
23935
|
+
supportedEvents: CORTEXCODE_HOOK_EVENTS,
|
|
23936
|
+
canonicalToToolEventNames: CANONICAL_TO_CORTEXCODE_EVENT_NAMES,
|
|
23937
|
+
toolToCanonicalEventNames: CORTEXCODE_TO_CANONICAL_EVENT_NAMES,
|
|
23938
|
+
projectDirVar: "$CORTEX_PROJECT_DIR",
|
|
23939
|
+
prefixDotRelativeCommandsOnly: true,
|
|
23940
|
+
noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
|
|
23941
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
23942
|
+
booleanPassthroughFields: [{
|
|
23943
|
+
canonical: "enabled",
|
|
23944
|
+
tool: "enabled"
|
|
23945
|
+
}]
|
|
23946
|
+
}
|
|
23638
23947
|
};
|
|
23639
23948
|
/**
|
|
23640
|
-
* Single spelling of the settings/hooks codec/policy: fail closed on an
|
|
23641
|
-
* unparseable root rather than replacing the user's Cortex Code settings with
|
|
23642
|
-
* generated output.
|
|
23643
|
-
*/
|
|
23644
|
-
function parseCortexcodeSettings(fileContent, filePath) {
|
|
23645
|
-
return parseSharedConfig({
|
|
23646
|
-
format: "json",
|
|
23647
|
-
fileContent,
|
|
23648
|
-
filePath,
|
|
23649
|
-
invalidRootPolicy: "error"
|
|
23650
|
-
});
|
|
23651
|
-
}
|
|
23652
|
-
/**
|
|
23653
23949
|
* Snowflake Cortex Code hooks.
|
|
23654
23950
|
*
|
|
23655
23951
|
* Hooks live under the top-level `hooks` key of `<project>/.cortex/settings.json`
|
|
@@ -23658,20 +23954,16 @@ function parseCortexcodeSettings(fileContent, filePath) {
|
|
|
23658
23954
|
* "<regex>", "hooks": [{ "type": "command" | "prompt", ..., "timeout"?:
|
|
23659
23955
|
* <seconds>, "enabled"?: <boolean> }] }] }`. The project file also holds
|
|
23660
23956
|
* settings rulesync does not own, so generation merges the `hooks` key into
|
|
23661
|
-
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it
|
|
23957
|
+
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it, and
|
|
23958
|
+
* neither file is removed wholesale (hooks.json sits in the CLI-owned
|
|
23959
|
+
* `~/.snowflake/cortex/` tree).
|
|
23662
23960
|
*
|
|
23663
23961
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/extensibility
|
|
23664
23962
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/settings
|
|
23665
23963
|
*/
|
|
23666
|
-
var CortexcodeHooks = class
|
|
23667
|
-
|
|
23668
|
-
|
|
23669
|
-
...params,
|
|
23670
|
-
fileContent: params.fileContent ?? "{}"
|
|
23671
|
-
});
|
|
23672
|
-
}
|
|
23673
|
-
isDeletable() {
|
|
23674
|
-
return false;
|
|
23964
|
+
var CortexcodeHooks = class extends SettingsJsonHooks {
|
|
23965
|
+
static getSpec() {
|
|
23966
|
+
return CORTEXCODE_SPEC;
|
|
23675
23967
|
}
|
|
23676
23968
|
static getSettablePaths({ global = false } = {}) {
|
|
23677
23969
|
return global ? {
|
|
@@ -23682,60 +23974,338 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23682
23974
|
relativeFilePath: CORTEXCODE_SETTINGS_FILE_NAME
|
|
23683
23975
|
};
|
|
23684
23976
|
}
|
|
23977
|
+
};
|
|
23978
|
+
//#endregion
|
|
23979
|
+
//#region src/constants/crush-paths.ts
|
|
23980
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
23981
|
+
const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
|
|
23982
|
+
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
23983
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
23984
|
+
const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
|
|
23985
|
+
const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
|
|
23986
|
+
const CRUSH_CONFIG_FILE_NAME = "crush.json";
|
|
23987
|
+
const CRUSH_HIDDEN_CONFIG_FILE_NAME = ".crush.json";
|
|
23988
|
+
const CRUSH_PERMISSIONS_KEY = "permissions";
|
|
23989
|
+
const CRUSH_ALLOWED_TOOLS_KEY = "allowed_tools";
|
|
23990
|
+
const CRUSH_OPTIONS_KEY = "options";
|
|
23991
|
+
const CRUSH_DISABLED_TOOLS_KEY = "disabled_tools";
|
|
23992
|
+
const CRUSH_HOOKS_KEY = "hooks";
|
|
23993
|
+
//#endregion
|
|
23994
|
+
//#region src/features/crush-config.ts
|
|
23995
|
+
/**
|
|
23996
|
+
* Where Crush's JSON config lives for a scope, before the project-scope twin
|
|
23997
|
+
* is resolved: `<project>/crush.json` or `~/.config/crush/crush.json`. This is
|
|
23998
|
+
* also the key every adapter reports through `getSettablePaths`, so the
|
|
23999
|
+
* shared-config ownership declaration, the gitignore derivation and the
|
|
24000
|
+
* shared-write ordering all see one file per scope.
|
|
24001
|
+
*/
|
|
24002
|
+
function getCrushConfigSettablePaths({ global = false } = {}) {
|
|
24003
|
+
return {
|
|
24004
|
+
relativeDirPath: global ? CRUSH_GLOBAL_DIR : ".",
|
|
24005
|
+
relativeFilePath: CRUSH_CONFIG_FILE_NAME
|
|
24006
|
+
};
|
|
24007
|
+
}
|
|
24008
|
+
/**
|
|
24009
|
+
* Resolves which Crush config file an adapter should read and write.
|
|
24010
|
+
*
|
|
24011
|
+
* At project scope Crush discovers both `.crush.json` and `crush.json` in the
|
|
24012
|
+
* working directory and merges every file it finds with `.crush.json` on top
|
|
24013
|
+
* (objects merge recursively, lists are concatenated, scalars are overridden).
|
|
24014
|
+
* Neither file therefore hides the other: an entry left in `crush.json` stays
|
|
24015
|
+
* in effect next to whatever `.crush.json` says. An existing `.crush.json` is
|
|
24016
|
+
* preferred because the user chose that spelling; otherwise `crush.json` is
|
|
24017
|
+
* used (and created when neither exists). The global scope has a single
|
|
24018
|
+
* spelling.
|
|
24019
|
+
*
|
|
24020
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
24021
|
+
*/
|
|
24022
|
+
async function resolveCrushConfigFile({ outputRoot, global = false }) {
|
|
24023
|
+
const paths = getCrushConfigSettablePaths({ global });
|
|
24024
|
+
const configDir = (0, node_path.join)(outputRoot, paths.relativeDirPath);
|
|
24025
|
+
const filePath = (0, node_path.join)(configDir, paths.relativeFilePath);
|
|
24026
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
24027
|
+
if (!global) {
|
|
24028
|
+
const hiddenPath = (0, node_path.join)(configDir, CRUSH_HIDDEN_CONFIG_FILE_NAME);
|
|
24029
|
+
const hiddenContent = await readFileContentOrNull(hiddenPath);
|
|
24030
|
+
if (hiddenContent !== null) return {
|
|
24031
|
+
relativeDirPath: paths.relativeDirPath,
|
|
24032
|
+
relativeFilePath: CRUSH_HIDDEN_CONFIG_FILE_NAME,
|
|
24033
|
+
filePath: hiddenPath,
|
|
24034
|
+
fileContent: hiddenContent,
|
|
24035
|
+
...fileContent === null ? {} : { twin: {
|
|
24036
|
+
filePath,
|
|
24037
|
+
fileContent
|
|
24038
|
+
} }
|
|
24039
|
+
};
|
|
24040
|
+
}
|
|
24041
|
+
return {
|
|
24042
|
+
relativeDirPath: paths.relativeDirPath,
|
|
24043
|
+
relativeFilePath: paths.relativeFilePath,
|
|
24044
|
+
filePath,
|
|
24045
|
+
fileContent
|
|
24046
|
+
};
|
|
24047
|
+
}
|
|
24048
|
+
/**
|
|
24049
|
+
* Single spelling of the crush.json codec/policy, matching the
|
|
24050
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
24051
|
+
* unparseable root rather than replacing the user's Crush config with
|
|
24052
|
+
* generated output.
|
|
24053
|
+
*/
|
|
24054
|
+
function parseCrushConfig(fileContent, filePath) {
|
|
24055
|
+
return parseSharedConfig({
|
|
24056
|
+
format: "json",
|
|
24057
|
+
fileContent,
|
|
24058
|
+
filePath,
|
|
24059
|
+
invalidRootPolicy: "error"
|
|
24060
|
+
});
|
|
24061
|
+
}
|
|
24062
|
+
/**
|
|
24063
|
+
* Combine two Crush config documents the way Crush itself does
|
|
24064
|
+
* (`github.com/qjebbs/go-jsons`): objects merge recursively, arrays are
|
|
24065
|
+
* concatenated with `base` first, and any other value from `override` wins.
|
|
24066
|
+
*/
|
|
24067
|
+
function mergeCrushConfigs({ base, override }) {
|
|
24068
|
+
const merged = { ...base };
|
|
24069
|
+
for (const [key, value] of Object.entries(override)) {
|
|
24070
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
24071
|
+
const current = merged[key];
|
|
24072
|
+
if (isRecord$1(current) && isRecord$1(value)) merged[key] = mergeCrushConfigs({
|
|
24073
|
+
base: current,
|
|
24074
|
+
override: value
|
|
24075
|
+
});
|
|
24076
|
+
else if (Array.isArray(current) && Array.isArray(value)) merged[key] = [...current, ...value];
|
|
24077
|
+
else merged[key] = value;
|
|
24078
|
+
}
|
|
24079
|
+
return merged;
|
|
24080
|
+
}
|
|
24081
|
+
/**
|
|
24082
|
+
* The content an import should read for a resolved location: the chosen file
|
|
24083
|
+
* merged over its twin when both exist, so a server, hook or tool entry that
|
|
24084
|
+
* only the lower-priority `crush.json` declares is imported too — Crush sees
|
|
24085
|
+
* it, so rulesync should as well.
|
|
24086
|
+
*/
|
|
24087
|
+
function crushConfigImportContent(location) {
|
|
24088
|
+
const own = location.fileContent ?? "{}";
|
|
24089
|
+
if (location.twin === void 0) return own;
|
|
24090
|
+
const merged = mergeCrushConfigs({
|
|
24091
|
+
base: parseCrushConfig(location.twin.fileContent, location.twin.filePath),
|
|
24092
|
+
override: parseCrushConfig(own, location.filePath)
|
|
24093
|
+
});
|
|
24094
|
+
return JSON.stringify(merged, null, 2);
|
|
24095
|
+
}
|
|
24096
|
+
function lookupPath(root, path) {
|
|
24097
|
+
let current = root;
|
|
24098
|
+
for (const segment of path) {
|
|
24099
|
+
if (!isRecord$1(current) || !Object.hasOwn(current, segment)) return;
|
|
24100
|
+
current = current[segment];
|
|
24101
|
+
}
|
|
24102
|
+
return current;
|
|
24103
|
+
}
|
|
24104
|
+
function isNonEmptyValue(value) {
|
|
24105
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
24106
|
+
if (isRecord$1(value)) return Object.keys(value).length > 0;
|
|
24107
|
+
return value !== void 0;
|
|
24108
|
+
}
|
|
24109
|
+
/**
|
|
24110
|
+
* Warn when the twin Crush merges beneath the written file still carries a
|
|
24111
|
+
* value at one of the paths this feature owns. Rulesync only rewrites the
|
|
24112
|
+
* chosen file, and because Crush concatenates lists and merges objects across
|
|
24113
|
+
* the pair, such an entry — typically one an earlier generate wrote to
|
|
24114
|
+
* `crush.json` before the user added a `.crush.json` — stays in effect until
|
|
24115
|
+
* removed by hand, and cannot be retracted from `.rulesync/`.
|
|
24116
|
+
*/
|
|
24117
|
+
function warnCrushTwinLeftovers({ location, ownedPaths, logger }) {
|
|
24118
|
+
if (location.twin === void 0) return;
|
|
24119
|
+
let twin;
|
|
24120
|
+
try {
|
|
24121
|
+
twin = parseCrushConfig(location.twin.fileContent, location.twin.filePath);
|
|
24122
|
+
} catch {
|
|
24123
|
+
return;
|
|
24124
|
+
}
|
|
24125
|
+
const leftovers = ownedPaths.filter((path) => isNonEmptyValue(lookupPath(twin, path))).map((path) => `"${path.join(".")}"`);
|
|
24126
|
+
if (leftovers.length === 0) return;
|
|
24127
|
+
logger?.warn(`Crush merges ${location.twin.filePath} beneath ${location.filePath} (objects recursively, lists concatenated), so the ${leftovers.join(", ")} entries it still carries stay in effect and are not managed by rulesync; remove them from that file by hand.`);
|
|
24128
|
+
}
|
|
24129
|
+
//#endregion
|
|
24130
|
+
//#region src/features/hooks/crush-hooks.ts
|
|
24131
|
+
const SUPPORTED_CRUSH_EVENTS = new Set(CRUSH_HOOK_EVENTS);
|
|
24132
|
+
/**
|
|
24133
|
+
* Crush matches an event key case-insensitively and ignores underscores
|
|
24134
|
+
* (`PreToolUse`, `pre_tool_use` and `PRE_TOOL_USE` all work), so an import
|
|
24135
|
+
* looks the canonical name up by that normalized spelling.
|
|
24136
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
24137
|
+
*/
|
|
24138
|
+
const NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CRUSH_TO_CANONICAL_EVENT_NAMES).map(([crushEvent, canonical]) => [normalizeCrushEventName(crushEvent), canonical]));
|
|
24139
|
+
function normalizeCrushEventName(event) {
|
|
24140
|
+
return event.replaceAll("_", "").toLowerCase();
|
|
24141
|
+
}
|
|
24142
|
+
/**
|
|
24143
|
+
* Build the `hooks` block of `crush.json` from a canonical hooks config.
|
|
24144
|
+
* Crush keys a flat array of `{name, matcher, command, timeout}` entries by
|
|
24145
|
+
* event name; `matcher` is a regex tested against the (lower-case) Crush tool
|
|
24146
|
+
* name and `timeout` is in seconds. Only `type: "command"` canonical hooks are
|
|
24147
|
+
* emitted, since a Crush hook is a shell command. A shared canonical event
|
|
24148
|
+
* Crush does not fire is skipped (the HooksProcessor reports it), while an
|
|
24149
|
+
* event under the `crush.hooks` override — such as one an import filed there
|
|
24150
|
+
* — is written verbatim so it round-trips.
|
|
24151
|
+
*/
|
|
24152
|
+
function canonicalToCrushHooks({ config, toolOverride, logger }) {
|
|
24153
|
+
const sharedHooks = {};
|
|
24154
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_CRUSH_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
24155
|
+
const effective = {
|
|
24156
|
+
...sharedHooks,
|
|
24157
|
+
...toolOverride
|
|
24158
|
+
};
|
|
24159
|
+
const hooks = {};
|
|
24160
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
24161
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
24162
|
+
const crushEvent = lookupOwn({
|
|
24163
|
+
record: CANONICAL_TO_CRUSH_EVENT_NAMES,
|
|
24164
|
+
key: event
|
|
24165
|
+
}) ?? event;
|
|
24166
|
+
const entries = [];
|
|
24167
|
+
for (const def of defs) {
|
|
24168
|
+
const entry = canonicalDefToCrushEntry({
|
|
24169
|
+
def,
|
|
24170
|
+
event,
|
|
24171
|
+
logger
|
|
24172
|
+
});
|
|
24173
|
+
if (entry !== null) entries.push(entry);
|
|
24174
|
+
}
|
|
24175
|
+
if (entries.length > 0) hooks[crushEvent] = entries;
|
|
24176
|
+
}
|
|
24177
|
+
return hooks;
|
|
24178
|
+
}
|
|
24179
|
+
/** Convert one canonical hook definition to a Crush entry, or null to skip. */
|
|
24180
|
+
function canonicalDefToCrushEntry({ def, event, logger }) {
|
|
24181
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
24182
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
24183
|
+
logger?.warn(`Crush hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Crush would discard, so it was skipped.`);
|
|
24184
|
+
return null;
|
|
24185
|
+
}
|
|
24186
|
+
const entry = { command: def.command };
|
|
24187
|
+
if (typeof def.name === "string" && def.name !== "") entry.name = def.name;
|
|
24188
|
+
if (typeof def.matcher === "string" && def.matcher !== "" && def.matcher !== "*") entry.matcher = def.matcher;
|
|
24189
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
24190
|
+
return entry;
|
|
24191
|
+
}
|
|
24192
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
24193
|
+
function crushEntryToCanonicalDef(raw) {
|
|
24194
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string") return null;
|
|
24195
|
+
const def = {
|
|
24196
|
+
type: "command",
|
|
24197
|
+
command: raw.command
|
|
24198
|
+
};
|
|
24199
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
24200
|
+
if (typeof raw.matcher === "string" && raw.matcher !== "") def.matcher = raw.matcher;
|
|
24201
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
24202
|
+
return def;
|
|
24203
|
+
}
|
|
24204
|
+
/**
|
|
24205
|
+
* Reverse {@link canonicalToCrushHooks}: parse the `hooks` block back into a
|
|
24206
|
+
* canonical event → definition[] record. An event Crush does not document is
|
|
24207
|
+
* carried under its own name so `buildImportedHooksConfig` files it under the
|
|
24208
|
+
* `crush.hooks` override.
|
|
24209
|
+
*/
|
|
24210
|
+
function crushHooksToCanonical(hooksBlock) {
|
|
24211
|
+
const canonical = {};
|
|
24212
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
24213
|
+
for (const [crushEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
24214
|
+
if (isPrototypePollutionKey(crushEvent) || !Array.isArray(rawEntries)) continue;
|
|
24215
|
+
const canonicalEvent = lookupOwn({
|
|
24216
|
+
record: NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES,
|
|
24217
|
+
key: normalizeCrushEventName(crushEvent)
|
|
24218
|
+
}) ?? crushEvent;
|
|
24219
|
+
const defs = rawEntries.map((raw) => crushEntryToCanonicalDef(raw)).filter((def) => def !== null);
|
|
24220
|
+
if (defs.length === 0) continue;
|
|
24221
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
24222
|
+
record: canonical,
|
|
24223
|
+
key: canonicalEvent
|
|
24224
|
+
}) ?? [], ...defs];
|
|
24225
|
+
}
|
|
24226
|
+
return canonical;
|
|
24227
|
+
}
|
|
24228
|
+
/**
|
|
24229
|
+
* Crush hooks.
|
|
24230
|
+
*
|
|
24231
|
+
* Crush reads hooks from the `hooks` key of its JSON config —
|
|
24232
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
24233
|
+
* lists concatenated) at project scope and `~/.config/crush/crush.json` at
|
|
24234
|
+
* user scope — as
|
|
24235
|
+
* `hooks.<Event>: [{name?, matcher?, command, timeout?}]`. Only `PreToolUse`
|
|
24236
|
+
* fires today. The `hooks` key is owned outright; every other top-level key
|
|
24237
|
+
* of the file is preserved and the file is never deleted.
|
|
24238
|
+
*
|
|
24239
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
24240
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
24241
|
+
*/
|
|
24242
|
+
var CrushHooks = class CrushHooks extends ToolHooks {
|
|
24243
|
+
json;
|
|
24244
|
+
constructor(params) {
|
|
24245
|
+
super(params);
|
|
24246
|
+
this.json = parseCrushConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
24247
|
+
}
|
|
24248
|
+
getJson() {
|
|
24249
|
+
return this.json;
|
|
24250
|
+
}
|
|
24251
|
+
isDeletable() {
|
|
24252
|
+
return false;
|
|
24253
|
+
}
|
|
24254
|
+
static getSettablePaths({ global = false } = {}) {
|
|
24255
|
+
return getCrushConfigSettablePaths({ global });
|
|
24256
|
+
}
|
|
23685
24257
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
23686
|
-
const
|
|
23687
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
23688
|
-
return new CortexcodeHooks({
|
|
24258
|
+
const location = await resolveCrushConfigFile({
|
|
23689
24259
|
outputRoot,
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
24260
|
+
global
|
|
24261
|
+
});
|
|
24262
|
+
return new CrushHooks({
|
|
24263
|
+
outputRoot,
|
|
24264
|
+
relativeDirPath: location.relativeDirPath,
|
|
24265
|
+
relativeFilePath: location.relativeFilePath,
|
|
24266
|
+
fileContent: crushConfigImportContent(location),
|
|
24267
|
+
validate,
|
|
24268
|
+
global
|
|
23694
24269
|
});
|
|
23695
24270
|
}
|
|
23696
24271
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
23697
|
-
const
|
|
23698
|
-
|
|
23699
|
-
|
|
23700
|
-
|
|
23701
|
-
const
|
|
23702
|
-
|
|
23703
|
-
|
|
23704
|
-
|
|
24272
|
+
const location = await resolveCrushConfigFile({
|
|
24273
|
+
outputRoot,
|
|
24274
|
+
global
|
|
24275
|
+
});
|
|
24276
|
+
const existingContent = location.fileContent ?? "";
|
|
24277
|
+
warnCrushTwinLeftovers({
|
|
24278
|
+
location,
|
|
24279
|
+
ownedPaths: [[CRUSH_HOOKS_KEY]],
|
|
23705
24280
|
logger
|
|
23706
24281
|
});
|
|
23707
|
-
const
|
|
23708
|
-
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
patch: { hooks },
|
|
23712
|
-
filePath,
|
|
24282
|
+
const config = rulesyncHooks.getJson();
|
|
24283
|
+
const hooks = canonicalToCrushHooks({
|
|
24284
|
+
config,
|
|
24285
|
+
toolOverride: config.crush?.hooks,
|
|
23713
24286
|
logger
|
|
23714
24287
|
});
|
|
23715
|
-
return new
|
|
24288
|
+
return new CrushHooks({
|
|
23716
24289
|
outputRoot,
|
|
23717
|
-
relativeDirPath:
|
|
23718
|
-
relativeFilePath:
|
|
23719
|
-
fileContent
|
|
23720
|
-
|
|
24290
|
+
relativeDirPath: location.relativeDirPath,
|
|
24291
|
+
relativeFilePath: location.relativeFilePath,
|
|
24292
|
+
fileContent: applySharedConfigPatch({
|
|
24293
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
24294
|
+
feature: "hooks",
|
|
24295
|
+
existingContent,
|
|
24296
|
+
patch: { [CRUSH_HOOKS_KEY]: hooks },
|
|
24297
|
+
filePath: location.filePath,
|
|
24298
|
+
logger
|
|
24299
|
+
}),
|
|
24300
|
+
validate,
|
|
24301
|
+
global
|
|
23721
24302
|
});
|
|
23722
24303
|
}
|
|
23723
|
-
toRulesyncHooks(
|
|
23724
|
-
const
|
|
23725
|
-
let settings;
|
|
23726
|
-
try {
|
|
23727
|
-
settings = parseCortexcodeSettings(this.getFileContent(), configPath);
|
|
23728
|
-
} catch (error) {
|
|
23729
|
-
throw new Error(`Failed to parse Cortex Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
23730
|
-
}
|
|
23731
|
-
const hooks = toolHooksToCanonical({
|
|
23732
|
-
logger,
|
|
23733
|
-
hooks: settings.hooks,
|
|
23734
|
-
converterConfig: CORTEXCODE_CONVERTER_CONFIG
|
|
23735
|
-
});
|
|
24304
|
+
toRulesyncHooks() {
|
|
24305
|
+
const hooks = crushHooksToCanonical(this.json[CRUSH_HOOKS_KEY]);
|
|
23736
24306
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
23737
24307
|
hooks,
|
|
23738
|
-
overrideKey: "
|
|
24308
|
+
overrideKey: "crush"
|
|
23739
24309
|
}), null, 2) });
|
|
23740
24310
|
}
|
|
23741
24311
|
validate() {
|
|
@@ -23744,13 +24314,14 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23744
24314
|
error: null
|
|
23745
24315
|
};
|
|
23746
24316
|
}
|
|
23747
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23748
|
-
return new
|
|
24317
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
24318
|
+
return new CrushHooks({
|
|
23749
24319
|
outputRoot,
|
|
23750
24320
|
relativeDirPath,
|
|
23751
24321
|
relativeFilePath,
|
|
23752
|
-
fileContent: JSON.stringify({
|
|
23753
|
-
validate: false
|
|
24322
|
+
fileContent: JSON.stringify({ [CRUSH_HOOKS_KEY]: {} }, null, 2),
|
|
24323
|
+
validate: false,
|
|
24324
|
+
global
|
|
23754
24325
|
});
|
|
23755
24326
|
}
|
|
23756
24327
|
};
|
|
@@ -26984,19 +27555,22 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
|
26984
27555
|
//#endregion
|
|
26985
27556
|
//#region src/features/hooks/tabnine-hooks.ts
|
|
26986
27557
|
/**
|
|
26987
|
-
* Environment block safe to hand Tabnine for a hook.
|
|
26988
|
-
*
|
|
26989
|
-
*
|
|
26990
|
-
*
|
|
26991
|
-
*
|
|
27558
|
+
* Environment block safe to hand Tabnine for a hook. The shared converter
|
|
27559
|
+
* refuses a whole `env` map holding one bad entry; Tabnine's shape is authored
|
|
27560
|
+
* outside it, so the same per-entry rule (`isSafeEnvEntry`: a key holding `=`,
|
|
27561
|
+
* a control character or nothing at all names a different variable than it
|
|
27562
|
+
* appears to) is applied entry by entry and the bad ones are dropped in both
|
|
27563
|
+
* directions, with a warning on export, where the value came from an authored
|
|
27564
|
+
* `.rulesync/hooks.*`.
|
|
26992
27565
|
*/
|
|
26993
27566
|
function sanitizeEnv({ env, warn }) {
|
|
26994
27567
|
if (env === null || env === void 0 || typeof env !== "object" || Array.isArray(env)) return;
|
|
26995
27568
|
const result = {};
|
|
26996
27569
|
for (const [key, value] of Object.entries(env)) {
|
|
26997
|
-
|
|
26998
|
-
|
|
26999
|
-
|
|
27570
|
+
if (!isSafeEnvEntry({
|
|
27571
|
+
key,
|
|
27572
|
+
value
|
|
27573
|
+
})) {
|
|
27000
27574
|
warn?.(`Tabnine CLI hook env entry ${JSON.stringify(key)} is not a safe KEY=VALUE pair; skipping it.`);
|
|
27001
27575
|
continue;
|
|
27002
27576
|
}
|
|
@@ -27097,7 +27671,7 @@ const TabnineMatcherEntrySchema = zod_mini.z.looseObject({
|
|
|
27097
27671
|
});
|
|
27098
27672
|
function tabnineMatcherEntryToCanonical(entry) {
|
|
27099
27673
|
const sequential = entry.sequential === true;
|
|
27100
|
-
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
27674
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && entry.matcher !== ".*" ? entry.matcher : void 0;
|
|
27101
27675
|
const defs = [];
|
|
27102
27676
|
for (const hook of entry.hooks ?? []) {
|
|
27103
27677
|
if (hook.type !== "command" || typeof hook.command !== "string" || hook.command === "") continue;
|
|
@@ -27818,6 +28392,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
27818
28392
|
supportedHookTypes: ["command"],
|
|
27819
28393
|
supportsMatcher: true
|
|
27820
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
|
+
}],
|
|
27821
28407
|
["copilot", {
|
|
27822
28408
|
class: CopilotHooks,
|
|
27823
28409
|
meta: {
|
|
@@ -28139,6 +28725,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28139
28725
|
supportedHookTypes: ["command", "http"],
|
|
28140
28726
|
supportsMatcher: true
|
|
28141
28727
|
}],
|
|
28728
|
+
["crush", {
|
|
28729
|
+
class: CrushHooks,
|
|
28730
|
+
meta: {
|
|
28731
|
+
supportsProject: true,
|
|
28732
|
+
supportsGlobal: true,
|
|
28733
|
+
supportsImport: true
|
|
28734
|
+
},
|
|
28735
|
+
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
28736
|
+
supportedHookTypes: ["command"],
|
|
28737
|
+
supportsMatcher: true,
|
|
28738
|
+
passthroughOverrideEvents: true
|
|
28739
|
+
}],
|
|
28142
28740
|
["zcode", {
|
|
28143
28741
|
class: ZcodeHooks,
|
|
28144
28742
|
meta: {
|
|
@@ -28820,14 +29418,6 @@ var ContinueIgnore = class ContinueIgnore extends ToolIgnore {
|
|
|
28820
29418
|
}
|
|
28821
29419
|
};
|
|
28822
29420
|
//#endregion
|
|
28823
|
-
//#region src/constants/crush-paths.ts
|
|
28824
|
-
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
28825
|
-
const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
|
|
28826
|
-
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
28827
|
-
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
28828
|
-
const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
|
|
28829
|
-
const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
|
|
28830
|
-
//#endregion
|
|
28831
29421
|
//#region src/features/ignore/crush-ignore.ts
|
|
28832
29422
|
/**
|
|
28833
29423
|
* Ignore generator for Crush.
|
|
@@ -30829,9 +31419,9 @@ function asBobRemoteType(stated, url) {
|
|
|
30829
31419
|
* SSE server carries a bare `url`. The canonical `transport` alias and the
|
|
30830
31420
|
* Claude-style `httpUrl` alias are folded into `type`/`url`; `env`, `cwd`,
|
|
30831
31421
|
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through, as Bob
|
|
30832
|
-
* documents all of them
|
|
30833
|
-
*
|
|
30834
|
-
* the HTTP requests).
|
|
31422
|
+
* documents all of them, with prototype-pollution keys dropped at every
|
|
31423
|
+
* nesting level (Bob spreads `env` and `headers` into the process environment
|
|
31424
|
+
* and the HTTP requests).
|
|
30835
31425
|
*
|
|
30836
31426
|
* Bob Shell documents the same file with an `httpURL` key instead of
|
|
30837
31427
|
* `type` + `url` for streamable HTTP. rulesync writes the IDE spelling (the two
|
|
@@ -30899,7 +31489,7 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30899
31489
|
}
|
|
30900
31490
|
for (const [key, value] of Object.entries(rest)) {
|
|
30901
31491
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30902
|
-
converted[key] = (
|
|
31492
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30903
31493
|
}
|
|
30904
31494
|
result[serverName] = converted;
|
|
30905
31495
|
}
|
|
@@ -30910,7 +31500,9 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30910
31500
|
* (`type: "streamable-http"` + `url`) is already canonical and passes through;
|
|
30911
31501
|
* the Bob Shell spelling `httpURL` becomes `url` with `type: "http"`; a bare
|
|
30912
31502
|
* `url` is an SSE server in Bob, so it gains `type: "sse"` to keep that reading
|
|
30913
|
-
* on the next generate.
|
|
31503
|
+
* on the next generate. Every other field passes through with its
|
|
31504
|
+
* prototype-pollution keys dropped at every nesting level, mirroring the
|
|
31505
|
+
* generate side's handling of `env` / `headers`.
|
|
30914
31506
|
*/
|
|
30915
31507
|
function convertFromBobFormat(mcpServers) {
|
|
30916
31508
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -30925,7 +31517,7 @@ function convertFromBobFormat(mcpServers) {
|
|
|
30925
31517
|
if (typeof value === "string") httpURL = value;
|
|
30926
31518
|
continue;
|
|
30927
31519
|
}
|
|
30928
|
-
converted[key] = value;
|
|
31520
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30929
31521
|
}
|
|
30930
31522
|
if (httpURL !== void 0) {
|
|
30931
31523
|
converted.url = httpURL;
|
|
@@ -31669,6 +32261,254 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
31669
32261
|
}
|
|
31670
32262
|
};
|
|
31671
32263
|
//#endregion
|
|
32264
|
+
//#region src/features/mcp/commandcode-mcp.ts
|
|
32265
|
+
/**
|
|
32266
|
+
* Parse a Command Code MCP file. The project file is the `.mcp.json` other
|
|
32267
|
+
* agents (Claude Code among them) share, so a hand-written file is read as
|
|
32268
|
+
* JSONC to tolerate comments and trailing commas; malformed content or a
|
|
32269
|
+
* non-object root (`null`, an array, a scalar) fails closed rather than being
|
|
32270
|
+
* spread into the regenerated file.
|
|
32271
|
+
*/
|
|
32272
|
+
function parseCommandcodeMcpConfig({ fileContent, relativePath }) {
|
|
32273
|
+
let parsed;
|
|
32274
|
+
try {
|
|
32275
|
+
parsed = parseJsonc$6(fileContent);
|
|
32276
|
+
} catch (error) {
|
|
32277
|
+
throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
32278
|
+
}
|
|
32279
|
+
if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: expected a JSON object at the root`);
|
|
32280
|
+
return parsed;
|
|
32281
|
+
}
|
|
32282
|
+
/**
|
|
32283
|
+
* The remote transport Command Code reads a server as. Its loader accepts
|
|
32284
|
+
* `http` (streamable HTTP) and `sse` for a `url` server; a bare `url` defaults
|
|
32285
|
+
* to `http`, and the canonical `streamable-http` spelling is folded into it.
|
|
32286
|
+
* A `ws(s)://` URL or any other stated transport has no Command Code
|
|
32287
|
+
* equivalent, so `undefined` tells the caller to skip the server.
|
|
32288
|
+
* @see https://commandcode.ai/docs/mcp
|
|
32289
|
+
*/
|
|
32290
|
+
function asCommandcodeRemoteTransport(stated, url) {
|
|
32291
|
+
if (stated === "sse") return "sse";
|
|
32292
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
32293
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
32294
|
+
}
|
|
32295
|
+
/**
|
|
32296
|
+
* Convert the canonical server map to the shape Command Code's config schema
|
|
32297
|
+
* documents (used for the global `~/.commandcode/mcp.json` only — the project
|
|
32298
|
+
* `.mcp.json` is shared with Claude Code and written pass-through, see the
|
|
32299
|
+
* class doc): a remote server is `{ transport, url, headers?, env?, oauth? }`
|
|
32300
|
+
* and a stdio server is `{ transport: "stdio", command, args?, env? }`, each
|
|
32301
|
+
* with an optional `enabled` flag. The documented spelling is `transport`
|
|
32302
|
+
* (`type` is only accepted as an alias on read), so that is what is written,
|
|
32303
|
+
* and only the documented keys are emitted — `timeout`, rulesync-only fields
|
|
32304
|
+
* and anything else is left out. The canonical `disabled: true` becomes
|
|
32305
|
+
* Command Code's native `enabled: false` (the rulesync-source-only `enabled`
|
|
32306
|
+
* filter never reaches this function: `getMcpServers()` drops such a server).
|
|
32307
|
+
*
|
|
32308
|
+
* A server Command Code drops at load time — no transport at all, a remote
|
|
32309
|
+
* transport without a URL, a WebSocket URL, or a stdio entry without a
|
|
32310
|
+
* command — is skipped with a warning rather than written in a form the tool
|
|
32311
|
+
* would silently ignore.
|
|
32312
|
+
* @see https://commandcode.ai/docs/mcp
|
|
32313
|
+
*/
|
|
32314
|
+
function convertToCommandcodeFormat(mcpServers, logger) {
|
|
32315
|
+
const result = {};
|
|
32316
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32317
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32318
|
+
if (declaresNoTransport(serverConfig)) {
|
|
32319
|
+
warnAndSkipMcpServer({
|
|
32320
|
+
toolName: "Command Code",
|
|
32321
|
+
serverName,
|
|
32322
|
+
reason: "no transport",
|
|
32323
|
+
logger
|
|
32324
|
+
});
|
|
32325
|
+
continue;
|
|
32326
|
+
}
|
|
32327
|
+
const converted = isRemoteMcpServer(serverConfig) ? convertRemoteServer$1({
|
|
32328
|
+
serverName,
|
|
32329
|
+
serverConfig,
|
|
32330
|
+
logger
|
|
32331
|
+
}) : convertStdioServer$1({
|
|
32332
|
+
serverName,
|
|
32333
|
+
serverConfig,
|
|
32334
|
+
logger
|
|
32335
|
+
});
|
|
32336
|
+
if (converted === void 0) continue;
|
|
32337
|
+
if (serverConfig.disabled === true) converted.enabled = false;
|
|
32338
|
+
if (isRecord$1(serverConfig.env)) converted.env = omitPrototypePollutionKeys(serverConfig.env);
|
|
32339
|
+
result[serverName] = converted;
|
|
32340
|
+
}
|
|
32341
|
+
return result;
|
|
32342
|
+
}
|
|
32343
|
+
function convertRemoteServer$1({ serverName, serverConfig, logger }) {
|
|
32344
|
+
const url = resolveRemoteMcpUrl(serverConfig);
|
|
32345
|
+
if (!url) {
|
|
32346
|
+
warnAndSkipMcpServer({
|
|
32347
|
+
toolName: "Command Code",
|
|
32348
|
+
serverName,
|
|
32349
|
+
reason: "a remote transport without a url",
|
|
32350
|
+
logger
|
|
32351
|
+
});
|
|
32352
|
+
return;
|
|
32353
|
+
}
|
|
32354
|
+
const stated = serverConfig.type ?? serverConfig.transport;
|
|
32355
|
+
const transport = asCommandcodeRemoteTransport(typeof stated === "string" ? stated : void 0, url);
|
|
32356
|
+
if (transport === void 0) {
|
|
32357
|
+
warnAndSkipMcpServer({
|
|
32358
|
+
toolName: "Command Code",
|
|
32359
|
+
serverName,
|
|
32360
|
+
reason: stated === void 0 ? "a WebSocket url, which Command Code's remote transports (http and sse) cannot reach" : `the "${String(stated)}" transport, which Command Code does not offer for remote servers (only http and sse)`,
|
|
32361
|
+
logger
|
|
32362
|
+
});
|
|
32363
|
+
return;
|
|
32364
|
+
}
|
|
32365
|
+
const converted = {
|
|
32366
|
+
transport,
|
|
32367
|
+
url
|
|
32368
|
+
};
|
|
32369
|
+
if (isRecord$1(serverConfig.headers)) converted.headers = omitPrototypePollutionKeys(serverConfig.headers);
|
|
32370
|
+
if (isRecord$1(serverConfig.oauth)) converted.oauth = omitPrototypePollutionKeys(serverConfig.oauth);
|
|
32371
|
+
return converted;
|
|
32372
|
+
}
|
|
32373
|
+
function convertStdioServer$1({ serverName, serverConfig, logger }) {
|
|
32374
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
32375
|
+
if (!command) {
|
|
32376
|
+
warnAndSkipMcpServer({
|
|
32377
|
+
toolName: "Command Code",
|
|
32378
|
+
serverName,
|
|
32379
|
+
reason: "a stdio transport without a command",
|
|
32380
|
+
logger
|
|
32381
|
+
});
|
|
32382
|
+
return;
|
|
32383
|
+
}
|
|
32384
|
+
const converted = {
|
|
32385
|
+
transport: "stdio",
|
|
32386
|
+
command
|
|
32387
|
+
};
|
|
32388
|
+
if (args.length > 0) converted.args = args;
|
|
32389
|
+
return converted;
|
|
32390
|
+
}
|
|
32391
|
+
/**
|
|
32392
|
+
* Convert Command Code's server map back to the canonical shape. Both
|
|
32393
|
+
* spellings Command Code accepts (`transport` and its `type` alias, with
|
|
32394
|
+
* `command`/`url`) are already canonical, so entries pass through with only
|
|
32395
|
+
* prototype-pollution keys dropped; the native `enabled: false` maps to the
|
|
32396
|
+
* canonical `disabled: true` (a bare `enabled` would otherwise be read back
|
|
32397
|
+
* as rulesync's own generation filter and silently drop the server from every
|
|
32398
|
+
* other target).
|
|
32399
|
+
*/
|
|
32400
|
+
function convertFromCommandcodeFormat(mcpServers) {
|
|
32401
|
+
if (!isMcpServers(mcpServers)) return {};
|
|
32402
|
+
const result = {};
|
|
32403
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32404
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32405
|
+
const { enabled, ...rest } = omitPrototypePollutionKeys(serverConfig);
|
|
32406
|
+
result[serverName] = enabled === false ? {
|
|
32407
|
+
...rest,
|
|
32408
|
+
disabled: true
|
|
32409
|
+
} : rest;
|
|
32410
|
+
}
|
|
32411
|
+
return result;
|
|
32412
|
+
}
|
|
32413
|
+
/**
|
|
32414
|
+
* Command Code MCP configuration.
|
|
32415
|
+
*
|
|
32416
|
+
* Command Code reads `.mcp.json` at the project root (project scope, meant to
|
|
32417
|
+
* be committed) and `~/.commandcode/mcp.json` (user scope), both in the
|
|
32418
|
+
* `{ "mcpServers": { ... } }` shape the docs show; the per-machine local
|
|
32419
|
+
* scope under `~/.commandcode/projects/` is left to the tool. Top-level
|
|
32420
|
+
* sibling keys of an existing file are kept. The global file is not deleted
|
|
32421
|
+
* by `--delete` (it lives outside the project), while the project file is
|
|
32422
|
+
* rulesync's own and is.
|
|
32423
|
+
*
|
|
32424
|
+
* The project `.mcp.json` is the very file the `claudecode` target writes, so
|
|
32425
|
+
* at project scope the servers are written in the same pass-through shape
|
|
32426
|
+
* `ClaudecodeMcp` uses: whichever of the two targets generates last leaves
|
|
32427
|
+
* byte-identical content, and Command Code reads that shape natively (`type`
|
|
32428
|
+
* is an alias of `transport`, a bare `url` infers `http`, and unknown keys
|
|
32429
|
+
* are ignored). Only the global file — Command Code's own — gets the
|
|
32430
|
+
* `transport` / `enabled: false` rewrite of `convertToCommandcodeFormat`.
|
|
32431
|
+
*
|
|
32432
|
+
* @see https://commandcode.ai/docs/mcp
|
|
32433
|
+
*/
|
|
32434
|
+
var CommandcodeMcp = class CommandcodeMcp extends ToolMcp {
|
|
32435
|
+
json;
|
|
32436
|
+
constructor(params) {
|
|
32437
|
+
super(params);
|
|
32438
|
+
this.json = this.fileContent === void 0 ? {} : parseCommandcodeMcpConfig({
|
|
32439
|
+
fileContent: this.fileContent,
|
|
32440
|
+
relativePath: (0, node_path.join)(this.relativeDirPath, this.relativeFilePath)
|
|
32441
|
+
});
|
|
32442
|
+
}
|
|
32443
|
+
getJson() {
|
|
32444
|
+
return this.json;
|
|
32445
|
+
}
|
|
32446
|
+
isDeletable() {
|
|
32447
|
+
return !this.global;
|
|
32448
|
+
}
|
|
32449
|
+
static getSettablePaths({ global = false } = {}) {
|
|
32450
|
+
return global ? {
|
|
32451
|
+
relativeDirPath: COMMANDCODE_DIR,
|
|
32452
|
+
relativeFilePath: COMMANDCODE_GLOBAL_MCP_FILE_NAME
|
|
32453
|
+
} : {
|
|
32454
|
+
relativeDirPath: ".",
|
|
32455
|
+
relativeFilePath: COMMANDCODE_PROJECT_MCP_FILE_NAME
|
|
32456
|
+
};
|
|
32457
|
+
}
|
|
32458
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32459
|
+
const paths = this.getSettablePaths({ global });
|
|
32460
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32461
|
+
return new CommandcodeMcp({
|
|
32462
|
+
outputRoot,
|
|
32463
|
+
relativeDirPath: paths.relativeDirPath,
|
|
32464
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32465
|
+
fileContent,
|
|
32466
|
+
validate,
|
|
32467
|
+
global
|
|
32468
|
+
});
|
|
32469
|
+
}
|
|
32470
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
32471
|
+
const paths = this.getSettablePaths({ global });
|
|
32472
|
+
const json = parseCommandcodeMcpConfig({
|
|
32473
|
+
fileContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32474
|
+
relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
|
|
32475
|
+
});
|
|
32476
|
+
const mcpServers = global ? convertToCommandcodeFormat(rulesyncMcp.getMcpServers(), logger) : rulesyncMcp.getMcpServers();
|
|
32477
|
+
const commandcodeConfig = {
|
|
32478
|
+
...json,
|
|
32479
|
+
mcpServers
|
|
32480
|
+
};
|
|
32481
|
+
return new CommandcodeMcp({
|
|
32482
|
+
outputRoot,
|
|
32483
|
+
relativeDirPath: paths.relativeDirPath,
|
|
32484
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32485
|
+
fileContent: JSON.stringify(commandcodeConfig, null, 2),
|
|
32486
|
+
validate,
|
|
32487
|
+
global
|
|
32488
|
+
});
|
|
32489
|
+
}
|
|
32490
|
+
toRulesyncMcp() {
|
|
32491
|
+
const mcpServers = convertFromCommandcodeFormat(this.json.mcpServers);
|
|
32492
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
32493
|
+
}
|
|
32494
|
+
validate() {
|
|
32495
|
+
return {
|
|
32496
|
+
success: true,
|
|
32497
|
+
error: null
|
|
32498
|
+
};
|
|
32499
|
+
}
|
|
32500
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
32501
|
+
return new CommandcodeMcp({
|
|
32502
|
+
outputRoot,
|
|
32503
|
+
relativeDirPath,
|
|
32504
|
+
relativeFilePath,
|
|
32505
|
+
fileContent: "{}",
|
|
32506
|
+
validate: false,
|
|
32507
|
+
global
|
|
32508
|
+
});
|
|
32509
|
+
}
|
|
32510
|
+
};
|
|
32511
|
+
//#endregion
|
|
31672
32512
|
//#region src/features/mcp/continue-mcp.ts
|
|
31673
32513
|
/**
|
|
31674
32514
|
* Parse a Continue MCP file. Continue reads the files under `mcpServers/` as
|
|
@@ -32331,7 +33171,7 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32331
33171
|
}
|
|
32332
33172
|
for (const [key, value] of Object.entries(rest)) {
|
|
32333
33173
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32334
|
-
converted[key] = (
|
|
33174
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
32335
33175
|
}
|
|
32336
33176
|
result[serverName] = converted;
|
|
32337
33177
|
}
|
|
@@ -32340,14 +33180,15 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32340
33180
|
/**
|
|
32341
33181
|
* Convert Cortex Code's server map back to the canonical shape. The
|
|
32342
33182
|
* documented spelling (`type` + `command`/`url`) is already canonical, so
|
|
32343
|
-
* entries pass through with only prototype-pollution keys dropped
|
|
33183
|
+
* entries pass through with only prototype-pollution keys dropped — at every
|
|
33184
|
+
* nesting level, mirroring the generate side.
|
|
32344
33185
|
*/
|
|
32345
33186
|
function convertFromCortexcodeFormat(mcpServers) {
|
|
32346
33187
|
if (!isMcpServers(mcpServers)) return {};
|
|
32347
33188
|
const result = {};
|
|
32348
33189
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32349
33190
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32350
|
-
result[serverName] =
|
|
33191
|
+
result[serverName] = omitPrototypePollutionKeysDeep(serverConfig);
|
|
32351
33192
|
}
|
|
32352
33193
|
return result;
|
|
32353
33194
|
}
|
|
@@ -32386,19 +33227,12 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32386
33227
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32387
33228
|
if (!global) throw new Error(CORTEXCODE_GLOBAL_ONLY_MESSAGE);
|
|
32388
33229
|
const paths = this.getSettablePaths({ global });
|
|
32389
|
-
const
|
|
32390
|
-
fileContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32391
|
-
relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
|
|
32392
|
-
});
|
|
32393
|
-
const newJson = {
|
|
32394
|
-
...json,
|
|
32395
|
-
mcpServers: json.mcpServers ?? {}
|
|
32396
|
-
};
|
|
33230
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32397
33231
|
return new CortexcodeMcp({
|
|
32398
33232
|
outputRoot,
|
|
32399
33233
|
relativeDirPath: paths.relativeDirPath,
|
|
32400
33234
|
relativeFilePath: paths.relativeFilePath,
|
|
32401
|
-
fileContent
|
|
33235
|
+
fileContent,
|
|
32402
33236
|
validate,
|
|
32403
33237
|
global
|
|
32404
33238
|
});
|
|
@@ -32446,6 +33280,319 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32446
33280
|
}
|
|
32447
33281
|
};
|
|
32448
33282
|
//#endregion
|
|
33283
|
+
//#region src/features/mcp/crush-mcp.ts
|
|
33284
|
+
/**
|
|
33285
|
+
* Crush-only MCP fields with no canonical counterpart, authored under a
|
|
33286
|
+
* `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
|
|
33287
|
+
* name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers and
|
|
33288
|
+
* `sessionless` marks a server that issues no `Mcp-Session-Id`.
|
|
33289
|
+
* `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
|
|
33290
|
+
* never reaches another tool's config; `fromRulesyncMcp` re-merges them from
|
|
33291
|
+
* the raw source JSON, honouring the raw Crush spelling as a fallback for an
|
|
33292
|
+
* entry copied straight out of a `crush.json` (the `experimental_environment`
|
|
33293
|
+
* precedent) — except `oauth`, whose canonical key is Claude Code's
|
|
33294
|
+
* `{ clientId }` object and which only `crushOauth` can set.
|
|
33295
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
33296
|
+
*/
|
|
33297
|
+
const CRUSH_ONLY_KEYS = [
|
|
33298
|
+
{
|
|
33299
|
+
canonical: "crushOauth",
|
|
33300
|
+
crush: "oauth",
|
|
33301
|
+
accepts: (v) => typeof v === "boolean",
|
|
33302
|
+
rawFallback: false
|
|
33303
|
+
},
|
|
33304
|
+
{
|
|
33305
|
+
canonical: "crushOauthClientId",
|
|
33306
|
+
crush: "oauth_client_id",
|
|
33307
|
+
accepts: (v) => typeof v === "string",
|
|
33308
|
+
rawFallback: true
|
|
33309
|
+
},
|
|
33310
|
+
{
|
|
33311
|
+
canonical: "crushOauthClientSecret",
|
|
33312
|
+
crush: "oauth_client_secret",
|
|
33313
|
+
accepts: (v) => typeof v === "string",
|
|
33314
|
+
rawFallback: true
|
|
33315
|
+
},
|
|
33316
|
+
{
|
|
33317
|
+
canonical: "crushOauthCallbackPort",
|
|
33318
|
+
crush: "oauth_callback_port",
|
|
33319
|
+
accepts: (v) => Number.isInteger(v),
|
|
33320
|
+
rawFallback: true
|
|
33321
|
+
},
|
|
33322
|
+
{
|
|
33323
|
+
canonical: "crushSessionless",
|
|
33324
|
+
crush: "sessionless",
|
|
33325
|
+
accepts: (v) => typeof v === "boolean",
|
|
33326
|
+
rawFallback: true
|
|
33327
|
+
}
|
|
33328
|
+
];
|
|
33329
|
+
/**
|
|
33330
|
+
* The Crush-only keys of one raw canonical server entry, keyed by their
|
|
33331
|
+
* canonical name, so `convertToCrushFormat` sees them after the shared map
|
|
33332
|
+
* stripped them.
|
|
33333
|
+
*/
|
|
33334
|
+
function readCrushOnlyKeys(rawServer) {
|
|
33335
|
+
const result = {};
|
|
33336
|
+
if (!isRecord$1(rawServer)) return result;
|
|
33337
|
+
for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
|
|
33338
|
+
const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
|
|
33339
|
+
if (value !== void 0) result[canonical] = value;
|
|
33340
|
+
}
|
|
33341
|
+
return result;
|
|
33342
|
+
}
|
|
33343
|
+
function copyCrushOnlyKeys({ from, to }) {
|
|
33344
|
+
for (const { canonical, crush, accepts } of CRUSH_ONLY_KEYS) if (accepts(from[canonical])) to[crush] = from[canonical];
|
|
33345
|
+
}
|
|
33346
|
+
/**
|
|
33347
|
+
* The remote transports Crush's `MCPType` enum accepts, spelled the way it
|
|
33348
|
+
* reads them. `streamable-http` is the canonical rulesync name for what Crush
|
|
33349
|
+
* calls `http`; a WebSocket server has no Crush transport and is left to the
|
|
33350
|
+
* caller to reject.
|
|
33351
|
+
*/
|
|
33352
|
+
function asCrushRemoteType(stated, url) {
|
|
33353
|
+
if (stated === "sse") return "sse";
|
|
33354
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
33355
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
33356
|
+
}
|
|
33357
|
+
/**
|
|
33358
|
+
* The transport half of a remote server (`type`, `url`, `headers`), or null
|
|
33359
|
+
* when Crush could not reach it and the server was skipped with a warning.
|
|
33360
|
+
*/
|
|
33361
|
+
function convertRemoteTransport({ name, config, logger }) {
|
|
33362
|
+
const url = resolveRemoteMcpUrl(config);
|
|
33363
|
+
if (!url) {
|
|
33364
|
+
warnAndSkipMcpServer({
|
|
33365
|
+
toolName: "Crush",
|
|
33366
|
+
serverName: name,
|
|
33367
|
+
reason: "a remote transport without a url",
|
|
33368
|
+
logger
|
|
33369
|
+
});
|
|
33370
|
+
return null;
|
|
33371
|
+
}
|
|
33372
|
+
const stated = config.type ?? config.transport;
|
|
33373
|
+
const type = asCrushRemoteType(stated, url);
|
|
33374
|
+
if (type === void 0) {
|
|
33375
|
+
warnAndSkipMcpServer({
|
|
33376
|
+
toolName: "Crush",
|
|
33377
|
+
serverName: name,
|
|
33378
|
+
reason: stated === void 0 ? "a WebSocket url, which Crush's remote transports (http and sse) cannot reach" : `the "${stated}" transport, which Crush does not offer for remote servers (only http and sse)`,
|
|
33379
|
+
logger
|
|
33380
|
+
});
|
|
33381
|
+
return null;
|
|
33382
|
+
}
|
|
33383
|
+
const converted = {
|
|
33384
|
+
type,
|
|
33385
|
+
url
|
|
33386
|
+
};
|
|
33387
|
+
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
33388
|
+
return converted;
|
|
33389
|
+
}
|
|
33390
|
+
/**
|
|
33391
|
+
* The transport half of a stdio server (`type`, `command`, `args`, `env`), or
|
|
33392
|
+
* null when it has no command and was skipped with a warning.
|
|
33393
|
+
*/
|
|
33394
|
+
function convertStdioTransport({ name, config, logger }) {
|
|
33395
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
33396
|
+
if (!command) {
|
|
33397
|
+
warnAndSkipMcpServer({
|
|
33398
|
+
toolName: "Crush",
|
|
33399
|
+
serverName: name,
|
|
33400
|
+
reason: "a stdio transport without a command",
|
|
33401
|
+
logger
|
|
33402
|
+
});
|
|
33403
|
+
return null;
|
|
33404
|
+
}
|
|
33405
|
+
const converted = {
|
|
33406
|
+
type: "stdio",
|
|
33407
|
+
command
|
|
33408
|
+
};
|
|
33409
|
+
if (args.length > 0) converted.args = args;
|
|
33410
|
+
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
33411
|
+
return converted;
|
|
33412
|
+
}
|
|
33413
|
+
/**
|
|
33414
|
+
* Convert canonical rulesync servers to the `mcp.<name>` shape of crush.json.
|
|
33415
|
+
* `type` is required by Crush's schema, so it is always written: stdio servers
|
|
33416
|
+
* carry `type: "stdio"` with `command`/`args`/`env`, remote servers `http` or
|
|
33417
|
+
* `sse` with `url` and optional `headers`. `disabled`, the per-server
|
|
33418
|
+
* `disabledTools`/`enabledTools` filters (`disabled_tools`/`enabled_tools`)
|
|
33419
|
+
* and `timeout` (which Crush reads in seconds) map onto their Crush fields.
|
|
33420
|
+
*
|
|
33421
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
33422
|
+
*/
|
|
33423
|
+
function convertToCrushFormat(mcpServers, logger) {
|
|
33424
|
+
const result = {};
|
|
33425
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
33426
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
33427
|
+
if (!isRecord$1(config)) continue;
|
|
33428
|
+
if (declaresNoTransport(config)) {
|
|
33429
|
+
warnAndSkipMcpServer({
|
|
33430
|
+
toolName: "Crush",
|
|
33431
|
+
serverName: name,
|
|
33432
|
+
reason: "no transport",
|
|
33433
|
+
logger
|
|
33434
|
+
});
|
|
33435
|
+
continue;
|
|
33436
|
+
}
|
|
33437
|
+
const converted = isRemoteMcpServer(config) ? convertRemoteTransport({
|
|
33438
|
+
name,
|
|
33439
|
+
config,
|
|
33440
|
+
logger
|
|
33441
|
+
}) : convertStdioTransport({
|
|
33442
|
+
name,
|
|
33443
|
+
config,
|
|
33444
|
+
logger
|
|
33445
|
+
});
|
|
33446
|
+
if (converted === null) continue;
|
|
33447
|
+
if (config.disabled === true) converted.disabled = true;
|
|
33448
|
+
if (config.disabledTools && config.disabledTools.length > 0) converted.disabled_tools = config.disabledTools;
|
|
33449
|
+
if (config.enabledTools && config.enabledTools.length > 0) converted.enabled_tools = config.enabledTools;
|
|
33450
|
+
if (typeof config.timeout === "number") converted.timeout = Math.ceil(config.timeout);
|
|
33451
|
+
copyCrushOnlyKeys({
|
|
33452
|
+
from: config,
|
|
33453
|
+
to: converted
|
|
33454
|
+
});
|
|
33455
|
+
result[name] = converted;
|
|
33456
|
+
}
|
|
33457
|
+
return result;
|
|
33458
|
+
}
|
|
33459
|
+
/**
|
|
33460
|
+
* Convert the `mcp.<name>` entries of crush.json back to canonical rulesync
|
|
33461
|
+
* servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
|
|
33462
|
+
* server with a `command` is stdio already); `http` and `sse` are kept as the
|
|
33463
|
+
* canonical transport names; `disabled_tools`/`enabled_tools` become the
|
|
33464
|
+
* canonical `disabledTools`/`enabledTools`; the Crush-only OAuth and
|
|
33465
|
+
* `sessionless` fields are lifted into their `crush*` authoring keys (a value
|
|
33466
|
+
* of the wrong type is dropped, as Crush itself would refuse it), and any
|
|
33467
|
+
* other key is kept verbatim so an import preserves what a hand-authored
|
|
33468
|
+
* entry declared.
|
|
33469
|
+
*/
|
|
33470
|
+
function convertFromCrushFormat(crushServers) {
|
|
33471
|
+
const result = {};
|
|
33472
|
+
for (const [name, config] of Object.entries(crushServers)) {
|
|
33473
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
33474
|
+
const converted = {};
|
|
33475
|
+
for (const [key, value] of Object.entries(config)) {
|
|
33476
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
33477
|
+
switch (key) {
|
|
33478
|
+
case "type":
|
|
33479
|
+
if (value === "http" || value === "sse") converted.type = value;
|
|
33480
|
+
break;
|
|
33481
|
+
case "disabled_tools":
|
|
33482
|
+
if (isStringArray$2(value)) converted.disabledTools = value;
|
|
33483
|
+
break;
|
|
33484
|
+
case "enabled_tools":
|
|
33485
|
+
if (isStringArray$2(value)) converted.enabledTools = value;
|
|
33486
|
+
break;
|
|
33487
|
+
case "oauth_token": break;
|
|
33488
|
+
default: {
|
|
33489
|
+
const crushOnly = CRUSH_ONLY_KEYS.find((entry) => entry.crush === key);
|
|
33490
|
+
if (crushOnly === void 0) converted[key] = value;
|
|
33491
|
+
else if (crushOnly.accepts(value)) converted[crushOnly.canonical] = value;
|
|
33492
|
+
}
|
|
33493
|
+
}
|
|
33494
|
+
}
|
|
33495
|
+
result[name] = converted;
|
|
33496
|
+
}
|
|
33497
|
+
return result;
|
|
33498
|
+
}
|
|
33499
|
+
/**
|
|
33500
|
+
* Crush MCP servers.
|
|
33501
|
+
*
|
|
33502
|
+
* Crush reads MCP servers from the `mcp` key of its JSON config:
|
|
33503
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
33504
|
+
* objects recursively) at project scope and `~/.config/crush/crush.json` at
|
|
33505
|
+
* user scope. The
|
|
33506
|
+
* `crushrc` Bash config that Crush now recommends compiles its `mcp add`
|
|
33507
|
+
* builtin into the same `mcp.<name>` entries and overrides the JSON key by
|
|
33508
|
+
* key, so a `crushrc` next to the generated file takes precedence. Every
|
|
33509
|
+
* other top-level key of the file is preserved; the file is never deleted.
|
|
33510
|
+
*
|
|
33511
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
33512
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
33513
|
+
*/
|
|
33514
|
+
var CrushMcp = class CrushMcp extends ToolMcp {
|
|
33515
|
+
json;
|
|
33516
|
+
constructor(params) {
|
|
33517
|
+
super(params);
|
|
33518
|
+
this.json = parseCrushConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
33519
|
+
}
|
|
33520
|
+
getJson() {
|
|
33521
|
+
return this.json;
|
|
33522
|
+
}
|
|
33523
|
+
isDeletable() {
|
|
33524
|
+
return false;
|
|
33525
|
+
}
|
|
33526
|
+
static getSettablePaths({ global = false } = {}) {
|
|
33527
|
+
return getCrushConfigSettablePaths({ global });
|
|
33528
|
+
}
|
|
33529
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
33530
|
+
const location = await resolveCrushConfigFile({
|
|
33531
|
+
outputRoot,
|
|
33532
|
+
global
|
|
33533
|
+
});
|
|
33534
|
+
return new CrushMcp({
|
|
33535
|
+
outputRoot,
|
|
33536
|
+
relativeDirPath: location.relativeDirPath,
|
|
33537
|
+
relativeFilePath: location.relativeFilePath,
|
|
33538
|
+
fileContent: crushConfigImportContent(location),
|
|
33539
|
+
validate,
|
|
33540
|
+
global
|
|
33541
|
+
});
|
|
33542
|
+
}
|
|
33543
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
33544
|
+
const location = await resolveCrushConfigFile({
|
|
33545
|
+
outputRoot,
|
|
33546
|
+
global
|
|
33547
|
+
});
|
|
33548
|
+
const existingContent = location.fileContent ?? "";
|
|
33549
|
+
warnCrushTwinLeftovers({
|
|
33550
|
+
location,
|
|
33551
|
+
ownedPaths: [["mcp"]],
|
|
33552
|
+
logger
|
|
33553
|
+
});
|
|
33554
|
+
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
|
|
33555
|
+
...serverConfig,
|
|
33556
|
+
...readCrushOnlyKeys(rulesyncMcp.getRawMcpServer(serverName))
|
|
33557
|
+
}])), logger);
|
|
33558
|
+
return new CrushMcp({
|
|
33559
|
+
outputRoot,
|
|
33560
|
+
relativeDirPath: location.relativeDirPath,
|
|
33561
|
+
relativeFilePath: location.relativeFilePath,
|
|
33562
|
+
fileContent: applySharedConfigPatch({
|
|
33563
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
33564
|
+
feature: "mcp",
|
|
33565
|
+
existingContent,
|
|
33566
|
+
patch: { ["mcp"]: converted },
|
|
33567
|
+
filePath: location.filePath,
|
|
33568
|
+
logger
|
|
33569
|
+
}),
|
|
33570
|
+
validate,
|
|
33571
|
+
global
|
|
33572
|
+
});
|
|
33573
|
+
}
|
|
33574
|
+
toRulesyncMcp() {
|
|
33575
|
+
const converted = convertFromCrushFormat(isRecord$1(this.json["mcp"]) ? this.json["mcp"] : {});
|
|
33576
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
33577
|
+
}
|
|
33578
|
+
validate() {
|
|
33579
|
+
return {
|
|
33580
|
+
success: true,
|
|
33581
|
+
error: null
|
|
33582
|
+
};
|
|
33583
|
+
}
|
|
33584
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
33585
|
+
return new CrushMcp({
|
|
33586
|
+
outputRoot,
|
|
33587
|
+
relativeDirPath,
|
|
33588
|
+
relativeFilePath,
|
|
33589
|
+
fileContent: JSON.stringify({ ["mcp"]: {} }, null, 2),
|
|
33590
|
+
validate: false,
|
|
33591
|
+
global
|
|
33592
|
+
});
|
|
33593
|
+
}
|
|
33594
|
+
};
|
|
33595
|
+
//#endregion
|
|
32449
33596
|
//#region src/features/mcp/mcp-env-var-format.ts
|
|
32450
33597
|
/**
|
|
32451
33598
|
* Canonical rulesync env var reference pattern: `${VAR}` (but not `${env:VAR}`,
|
|
@@ -36330,6 +37477,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
36330
37477
|
static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
|
|
36331
37478
|
const servers = rulesyncMcp.forTarget({
|
|
36332
37479
|
toolTarget: "rovodev",
|
|
37480
|
+
global,
|
|
36333
37481
|
logger
|
|
36334
37482
|
}).getMcpServers();
|
|
36335
37483
|
const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
|
|
@@ -36490,7 +37638,7 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36490
37638
|
}
|
|
36491
37639
|
for (const [key, value] of Object.entries(rest)) {
|
|
36492
37640
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36493
|
-
converted[key] = (
|
|
37641
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36494
37642
|
}
|
|
36495
37643
|
if (enabledTools !== void 0) converted.includeTools = enabledTools;
|
|
36496
37644
|
if (disabledTools !== void 0) converted.excludeTools = disabledTools;
|
|
@@ -36501,7 +37649,9 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36501
37649
|
/**
|
|
36502
37650
|
* Convert Tabnine's server map back to the canonical shape: `includeTools` /
|
|
36503
37651
|
* `excludeTools` become `enabledTools` / `disabledTools`; `type` (`stdio`,
|
|
36504
|
-
* `sse`, `http`) and every other documented field are already canonical
|
|
37652
|
+
* `sse`, `http`) and every other documented field are already canonical and
|
|
37653
|
+
* pass through with their prototype-pollution keys dropped at every nesting
|
|
37654
|
+
* level, mirroring the generate side.
|
|
36505
37655
|
*/
|
|
36506
37656
|
function convertFromTabnineFormat(mcpServers) {
|
|
36507
37657
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -36513,7 +37663,7 @@ function convertFromTabnineFormat(mcpServers) {
|
|
|
36513
37663
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36514
37664
|
if (key === "includeTools") converted.enabledTools = value;
|
|
36515
37665
|
else if (key === "excludeTools") converted.disabledTools = value;
|
|
36516
|
-
else converted[key] = value;
|
|
37666
|
+
else converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36517
37667
|
}
|
|
36518
37668
|
result[serverName] = converted;
|
|
36519
37669
|
}
|
|
@@ -37625,6 +38775,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37625
38775
|
supportsDisabledTools: true
|
|
37626
38776
|
}
|
|
37627
38777
|
}],
|
|
38778
|
+
["commandcode", {
|
|
38779
|
+
class: CommandcodeMcp,
|
|
38780
|
+
meta: {
|
|
38781
|
+
supportsProject: true,
|
|
38782
|
+
supportsGlobal: true,
|
|
38783
|
+
supportsEnabledTools: false,
|
|
38784
|
+
supportsDisabledTools: false
|
|
38785
|
+
}
|
|
38786
|
+
}],
|
|
37628
38787
|
["continue", {
|
|
37629
38788
|
class: ContinueMcp,
|
|
37630
38789
|
meta: {
|
|
@@ -37661,6 +38820,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37661
38820
|
supportsDisabledTools: false
|
|
37662
38821
|
}
|
|
37663
38822
|
}],
|
|
38823
|
+
["crush", {
|
|
38824
|
+
class: CrushMcp,
|
|
38825
|
+
meta: {
|
|
38826
|
+
supportsProject: true,
|
|
38827
|
+
supportsGlobal: true,
|
|
38828
|
+
supportsEnabledTools: true,
|
|
38829
|
+
supportsDisabledTools: true
|
|
38830
|
+
}
|
|
38831
|
+
}],
|
|
37664
38832
|
["cursor", {
|
|
37665
38833
|
class: CursorMcp,
|
|
37666
38834
|
meta: {
|
|
@@ -38009,6 +39177,7 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
38009
39177
|
const toolMcps = await Promise.all([rulesyncMcp].map(async (mcp) => {
|
|
38010
39178
|
const targetedRulesyncMcp = mcp.forTarget({
|
|
38011
39179
|
toolTarget: this.toolTarget,
|
|
39180
|
+
global: this.global,
|
|
38012
39181
|
logger: this.logger
|
|
38013
39182
|
});
|
|
38014
39183
|
const fieldsToStrip = [];
|
|
@@ -39092,7 +40261,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
39092
40261
|
function toAntigravityCliToolName(canonical) {
|
|
39093
40262
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
39094
40263
|
}
|
|
39095
|
-
function toCanonicalToolName$
|
|
40264
|
+
function toCanonicalToolName$8(cliName) {
|
|
39096
40265
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
39097
40266
|
}
|
|
39098
40267
|
/**
|
|
@@ -39308,7 +40477,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
39308
40477
|
const processEntries = (entries, action) => {
|
|
39309
40478
|
for (const entry of entries) {
|
|
39310
40479
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
39311
|
-
const canonical = toCanonicalToolName$
|
|
40480
|
+
const canonical = toCanonicalToolName$8(toolName);
|
|
39312
40481
|
if (!permission[canonical]) permission[canonical] = {};
|
|
39313
40482
|
permission[canonical][pattern] = action;
|
|
39314
40483
|
}
|
|
@@ -39565,11 +40734,34 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
|
|
|
39565
40734
|
websearch: "web-search"
|
|
39566
40735
|
};
|
|
39567
40736
|
const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
40737
|
+
const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
|
|
40738
|
+
terminal: "launch-process",
|
|
40739
|
+
read: "view",
|
|
40740
|
+
edit: "str-replace-editor",
|
|
40741
|
+
write: "save-file"
|
|
40742
|
+
};
|
|
39568
40743
|
function toAugmentToolName(canonical) {
|
|
39569
|
-
return
|
|
40744
|
+
return lookupOwn({
|
|
40745
|
+
record: CANONICAL_TO_AUGMENT_TOOL_NAMES,
|
|
40746
|
+
key: canonical
|
|
40747
|
+
}) ?? canonical;
|
|
39570
40748
|
}
|
|
39571
|
-
|
|
39572
|
-
|
|
40749
|
+
/**
|
|
40750
|
+
* Resolve an existing entry's `toolName` to the legacy spelling rulesync emits, so that the
|
|
40751
|
+
* current alias and the legacy name are treated as the same managed tool.
|
|
40752
|
+
*/
|
|
40753
|
+
function toLegacyAugmentToolName(augmentName) {
|
|
40754
|
+
return lookupOwn({
|
|
40755
|
+
record: CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES,
|
|
40756
|
+
key: augmentName
|
|
40757
|
+
}) ?? augmentName;
|
|
40758
|
+
}
|
|
40759
|
+
function toCanonicalToolName$7(augmentName) {
|
|
40760
|
+
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
40761
|
+
return lookupOwn({
|
|
40762
|
+
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
40763
|
+
key: legacyName
|
|
40764
|
+
}) ?? legacyName;
|
|
39573
40765
|
}
|
|
39574
40766
|
function actionToAugmentType(action) {
|
|
39575
40767
|
switch (action) {
|
|
@@ -39785,10 +40977,11 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
39785
40977
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
39786
40978
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
39787
40979
|
if (entry.toolName === "*") return false;
|
|
39788
|
-
|
|
40980
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40981
|
+
if (!MANAGED_AUGMENT_TOOL_NAMES.has(legacyToolName)) return true;
|
|
39789
40982
|
if (entry.permission.type === "deny") {
|
|
39790
|
-
const
|
|
39791
|
-
return !generatedKeys.has(
|
|
40983
|
+
const suffix = `|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
40984
|
+
return !generatedKeys.has(`${entry.toolName}${suffix}`) && !generatedKeys.has(`${legacyToolName}${suffix}`);
|
|
39792
40985
|
}
|
|
39793
40986
|
return false;
|
|
39794
40987
|
});
|
|
@@ -40015,14 +41208,15 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40015
41208
|
if (isSpecialEntry(entry)) continue;
|
|
40016
41209
|
const type = entry.permission.type;
|
|
40017
41210
|
if (!isBasicAugmentType(type)) continue;
|
|
40018
|
-
const
|
|
41211
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
41212
|
+
const canonical = toCanonicalToolName$7(entry.toolName);
|
|
40019
41213
|
if (forbiddenMapKeys.has(canonical)) {
|
|
40020
41214
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
|
|
40021
41215
|
continue;
|
|
40022
41216
|
}
|
|
40023
41217
|
const action = augmentTypeToAction(type);
|
|
40024
41218
|
let pattern;
|
|
40025
|
-
if (
|
|
41219
|
+
if (legacyToolName === "launch-process" && entry.shellInputRegex) {
|
|
40026
41220
|
const regex = entry.shellInputRegex;
|
|
40027
41221
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
40028
41222
|
else if (action === "deny") {
|
|
@@ -40037,9 +41231,19 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40037
41231
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because its pattern resolves to the reserved object key '${pattern}'.`);
|
|
40038
41232
|
continue;
|
|
40039
41233
|
}
|
|
40040
|
-
|
|
40041
|
-
|
|
40042
|
-
|
|
41234
|
+
let bucket = lookupOwn({
|
|
41235
|
+
record: permission,
|
|
41236
|
+
key: canonical
|
|
41237
|
+
});
|
|
41238
|
+
if (bucket === void 0) {
|
|
41239
|
+
bucket = {};
|
|
41240
|
+
permission[canonical] = bucket;
|
|
41241
|
+
}
|
|
41242
|
+
const existing = lookupOwn({
|
|
41243
|
+
record: bucket,
|
|
41244
|
+
key: pattern
|
|
41245
|
+
});
|
|
41246
|
+
if (existing === void 0 || actionPriority[action] > actionPriority[existing]) bucket[pattern] = action;
|
|
40043
41247
|
}
|
|
40044
41248
|
return { permission };
|
|
40045
41249
|
}
|
|
@@ -40250,7 +41454,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
40250
41454
|
function toClaudeToolName(canonical) {
|
|
40251
41455
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
40252
41456
|
}
|
|
40253
|
-
function toCanonicalToolName$
|
|
41457
|
+
function toCanonicalToolName$6(claudeName) {
|
|
40254
41458
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
40255
41459
|
}
|
|
40256
41460
|
/**
|
|
@@ -41145,7 +42349,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
41145
42349
|
const processEntries = (entries, action) => {
|
|
41146
42350
|
for (const entry of entries) {
|
|
41147
42351
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
41148
|
-
const canonical = toCanonicalToolName$
|
|
42352
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
41149
42353
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41150
42354
|
permission[canonical][pattern] = action;
|
|
41151
42355
|
}
|
|
@@ -41968,9 +43172,634 @@ function mapBashActionToDecision(action) {
|
|
|
41968
43172
|
return "forbidden";
|
|
41969
43173
|
}
|
|
41970
43174
|
//#endregion
|
|
43175
|
+
//#region src/features/permissions/commandcode-permissions.ts
|
|
43176
|
+
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
43177
|
+
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
43178
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
43179
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
43180
|
+
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
43181
|
+
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
43182
|
+
bash: "Shell",
|
|
43183
|
+
read: "Read",
|
|
43184
|
+
edit: "Edit",
|
|
43185
|
+
write: "Write",
|
|
43186
|
+
grep: "Grep",
|
|
43187
|
+
glob: "Glob",
|
|
43188
|
+
webfetch: "WebFetch",
|
|
43189
|
+
websearch: "WebSearch"
|
|
43190
|
+
};
|
|
43191
|
+
const COMMANDCODE_TOOL_TO_CATEGORY = {
|
|
43192
|
+
...Object.fromEntries(Object.entries(CATEGORY_TO_COMMANDCODE_TOOL).map(([category, tool]) => [tool.toLowerCase(), category])),
|
|
43193
|
+
bash: "bash",
|
|
43194
|
+
powershell: "bash",
|
|
43195
|
+
shell_command: "bash",
|
|
43196
|
+
monitor_command: "bash",
|
|
43197
|
+
kill_shell: "bash",
|
|
43198
|
+
notebookedit: "edit",
|
|
43199
|
+
write_file: "write",
|
|
43200
|
+
web_fetch: "webfetch",
|
|
43201
|
+
web_search: "websearch"
|
|
43202
|
+
};
|
|
43203
|
+
const ACTION_RANK$1 = {
|
|
43204
|
+
allow: 0,
|
|
43205
|
+
ask: 1,
|
|
43206
|
+
deny: 2
|
|
43207
|
+
};
|
|
43208
|
+
/** Whether `action` outranks `existing` (deny > ask > allow). */
|
|
43209
|
+
function isStricterAction({ action, existing }) {
|
|
43210
|
+
return existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing];
|
|
43211
|
+
}
|
|
43212
|
+
/**
|
|
43213
|
+
* Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
|
|
43214
|
+
* the whitespace makes it a pattern rule, and a shell pattern grants only
|
|
43215
|
+
* commands that pass Command Code's pattern gate.
|
|
43216
|
+
*/
|
|
43217
|
+
const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (the command must parse into words and carry no environment assignment)";
|
|
43218
|
+
/**
|
|
43219
|
+
* Build a Command Code rule (`Shell(git *)`, `Read`, `mcp__github__get_issue`,
|
|
43220
|
+
* `mcp__*`, `*`) from a canonical category + pattern. Returns `null` for
|
|
43221
|
+
* categories Command Code cannot express so the caller can skip them.
|
|
43222
|
+
*
|
|
43223
|
+
* MCP tools are keyed by their canonical `mcp__server__tool` name, so a
|
|
43224
|
+
* scoped `mcp__<remainder>` category is written verbatim and the bare `mcp`
|
|
43225
|
+
* category becomes `mcp__*` (or `mcp__<pattern>` when the pattern names a
|
|
43226
|
+
* server or tool). A pattern on an MCP category is still written as the
|
|
43227
|
+
* `(specifier)` here; `writableCommandcodeRule` decides what to do with it,
|
|
43228
|
+
* because Command Code ignores it. The all-tools `*` category is only
|
|
43229
|
+
* written for its catch-all pattern; narrower `*` patterns are shell
|
|
43230
|
+
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
43231
|
+
*/
|
|
43232
|
+
function buildCommandcodeRule(category, pattern) {
|
|
43233
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
|
|
43234
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
|
|
43235
|
+
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
|
|
43236
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
|
|
43237
|
+
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
43238
|
+
if (tool === void 0) return null;
|
|
43239
|
+
return catchAll ? tool : `${tool}(${pattern})`;
|
|
43240
|
+
}
|
|
43241
|
+
/**
|
|
43242
|
+
* Split `Tool(specifier)` into its two halves the way Command Code's
|
|
43243
|
+
* `splitRule` does (`command-code` 1.54.0): the rule is split at the first
|
|
43244
|
+
* unescaped `(` and must end with `)`, the tool half is trimmed (so
|
|
43245
|
+
* `Shell (rm -rf *)` is a `Shell` rule), and a `\(` / `\)` in the specifier
|
|
43246
|
+
* is an escaped parenthesis. `Tool` alone has an empty specifier. A rule that
|
|
43247
|
+
* opens a parenthesis without closing one is not a rule to Command Code at
|
|
43248
|
+
* all, so it comes back with an empty tool half and stays unmodeled. Nothing
|
|
43249
|
+
* else in the specifier is touched: Command Code does not trim it (the shell
|
|
43250
|
+
* matcher normalizes whitespace on its own, see `parseCommandcodeRule`), so
|
|
43251
|
+
* `Read( )` is a rule for the pattern `" "`, which matches nothing, not for
|
|
43252
|
+
* the whole tool.
|
|
43253
|
+
*/
|
|
43254
|
+
function splitCommandcodeRule(rule) {
|
|
43255
|
+
const trimmed = rule.trim();
|
|
43256
|
+
const parenIndex = trimmed.search(/(?<!\\)\(/);
|
|
43257
|
+
if (parenIndex === -1) return {
|
|
43258
|
+
tool: trimmed,
|
|
43259
|
+
inner: ""
|
|
43260
|
+
};
|
|
43261
|
+
if (!trimmed.endsWith(")")) return {
|
|
43262
|
+
tool: "",
|
|
43263
|
+
inner: ""
|
|
43264
|
+
};
|
|
43265
|
+
return {
|
|
43266
|
+
tool: trimmed.slice(0, parenIndex).trim(),
|
|
43267
|
+
inner: trimmed.slice(parenIndex + 1, -1).replace(/\\([()])/g, "$1")
|
|
43268
|
+
};
|
|
43269
|
+
}
|
|
43270
|
+
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
43271
|
+
function isCatchAllSpecifier(inner) {
|
|
43272
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$6;
|
|
43273
|
+
}
|
|
43274
|
+
/**
|
|
43275
|
+
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
43276
|
+
* `parsePermissionRule` / `parseMcpToken`). The MCP shape is only recognized
|
|
43277
|
+
* by its exact `mcp__` prefix, and such a rule never carries a specifier:
|
|
43278
|
+
* `mcp__<server>__<tool>(owner:foo)` denies, asks or allows the whole tool.
|
|
43279
|
+
* Its names are matched case-sensitively against the registered server and
|
|
43280
|
+
* tool (Command Code never lowercases them), so the remainder keeps its case
|
|
43281
|
+
* as the canonical category. `mcp__<server>`, `mcp__<server>__` and
|
|
43282
|
+
* `mcp__<server>__*` are the whole server (category `mcp__<server>`), and a
|
|
43283
|
+
* `*` server (`mcp__*`, `mcp__*__<tool>`) is every MCP tool, honored in
|
|
43284
|
+
* `deny`/`ask` only. A differently-cased `MCP__...` spelling is read as a
|
|
43285
|
+
* plain tool-name rule instead, matched case-insensitively:
|
|
43286
|
+
* `MCP__<server>__<tool>` matches that tool by name (so it folds onto the
|
|
43287
|
+
* `mcp__` category, remainder as written), `MCP__<server>__<tool>(specifier)`
|
|
43288
|
+
* globs the specifier against the call's `command` / `file_path` / `path` /
|
|
43289
|
+
* `url` / `pattern` argument (in `deny`/`ask` a `param:glob` form is tried
|
|
43290
|
+
* first) — kept as a pattern in `deny`/`ask`, but not imported from `allow`,
|
|
43291
|
+
* where a grant scoped that way cannot be modeled without widening it; the
|
|
43292
|
+
* globs
|
|
43293
|
+
* `MCP__*` (every MCP tool) and `MCP__<server>__*` (the whole server) match
|
|
43294
|
+
* by name in `deny`/`ask` only, and `MCP__<server>` names no tool at all.
|
|
43295
|
+
* Whatever matches nothing anywhere (or a glob rulesync does not model)
|
|
43296
|
+
* returns `null` and is left alone.
|
|
43297
|
+
*/
|
|
43298
|
+
function parseMcpCommandcodeRule({ tool, inner }) {
|
|
43299
|
+
const remainder = tool.slice(5);
|
|
43300
|
+
const separator = remainder.indexOf("__");
|
|
43301
|
+
const server = separator === -1 ? remainder : remainder.slice(0, separator);
|
|
43302
|
+
const rest = separator === -1 ? "" : remainder.slice(separator + 2);
|
|
43303
|
+
if (server.length === 0) return null;
|
|
43304
|
+
const everyServer = {
|
|
43305
|
+
category: "mcp",
|
|
43306
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43307
|
+
notInAllow: true
|
|
43308
|
+
};
|
|
43309
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
|
|
43310
|
+
if (server === CATCH_ALL_PATTERN$6) return everyServer;
|
|
43311
|
+
if (server.includes(CATCH_ALL_PATTERN$6)) return null;
|
|
43312
|
+
return {
|
|
43313
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
|
|
43314
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43315
|
+
notInAllow: false
|
|
43316
|
+
};
|
|
43317
|
+
}
|
|
43318
|
+
const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
|
|
43319
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
|
|
43320
|
+
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
43321
|
+
category,
|
|
43322
|
+
pattern: inner,
|
|
43323
|
+
notInAllow: true
|
|
43324
|
+
} : null;
|
|
43325
|
+
if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
|
|
43326
|
+
if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
|
|
43327
|
+
category: `${MCP_CANONICAL_PREFIX$5}${server}`,
|
|
43328
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43329
|
+
notInAllow: true
|
|
43330
|
+
};
|
|
43331
|
+
return namesTool ? {
|
|
43332
|
+
category,
|
|
43333
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43334
|
+
notInAllow: false
|
|
43335
|
+
} : null;
|
|
43336
|
+
}
|
|
43337
|
+
/**
|
|
43338
|
+
* A rule on one of the friendly tools, with its specifier read as Command
|
|
43339
|
+
* Code matches it. The shell matcher trims and collapses whitespace on both
|
|
43340
|
+
* the pattern and the command, so `Shell( git * )` is `Shell(git *)` and a
|
|
43341
|
+
* pattern that is blank once normalized (`Shell( )`), or a bare `:*` whose
|
|
43342
|
+
* prefix is empty (`Shell(:*)`), matches nothing. A padded `Shell( * )` is
|
|
43343
|
+
* not the bare `Shell`: only a specifier of exactly
|
|
43344
|
+
* `` or `*` is dropped by the rule parser, so `( * )` stays a pattern rule —
|
|
43345
|
+
* in `deny`/`ask` it matches every command (the whole tool), while in `allow`
|
|
43346
|
+
* it is narrower than the bare `Shell` (it needs a command that parses into
|
|
43347
|
+
* words and has no environment-assignment prefix), so it is the whole tool
|
|
43348
|
+
* marked `notInAllow`. The path, web and tool-name matchers use the
|
|
43349
|
+
* specifier as written, so one that is blank or `*` only once trimmed
|
|
43350
|
+
* (`Read( * )`) matches nothing. What matches nothing comes back as `null`:
|
|
43351
|
+
* the rule is unmodeled and left alone.
|
|
43352
|
+
*/
|
|
43353
|
+
function friendlyToolRule({ category, inner }) {
|
|
43354
|
+
if (category === "bash") {
|
|
43355
|
+
if (isCatchAllSpecifier(inner)) return {
|
|
43356
|
+
category,
|
|
43357
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43358
|
+
notInAllow: false
|
|
43359
|
+
};
|
|
43360
|
+
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
43361
|
+
if (normalized === "" || normalized === ":*") return null;
|
|
43362
|
+
return isCatchAllSpecifier(normalized) ? {
|
|
43363
|
+
category,
|
|
43364
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43365
|
+
notInAllow: true
|
|
43366
|
+
} : {
|
|
43367
|
+
category,
|
|
43368
|
+
pattern: normalized,
|
|
43369
|
+
notInAllow: false
|
|
43370
|
+
};
|
|
43371
|
+
}
|
|
43372
|
+
if (isCatchAllSpecifier(inner)) return {
|
|
43373
|
+
category,
|
|
43374
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43375
|
+
notInAllow: false
|
|
43376
|
+
};
|
|
43377
|
+
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
43378
|
+
category,
|
|
43379
|
+
pattern: inner,
|
|
43380
|
+
notInAllow: false
|
|
43381
|
+
};
|
|
43382
|
+
}
|
|
43383
|
+
/**
|
|
43384
|
+
* Parse a Command Code rule back into canonical terms. Tool names fold case;
|
|
43385
|
+
* `Tool`, `Tool()` and `Tool(*)` all mean the whole tool; MCP-shaped rules
|
|
43386
|
+
* go through `parseMcpCommandcodeRule`. Returns `null` for a rule rulesync
|
|
43387
|
+
* cannot model (an exact internal tool name such as `edit_file`, a
|
|
43388
|
+
* name-wildcard like `edit_*`, a specifier on a rule that takes none, or a
|
|
43389
|
+
* rule Command Code enforces nothing for, such as `Agent`).
|
|
43390
|
+
*/
|
|
43391
|
+
function parseCommandcodeRule(rule) {
|
|
43392
|
+
const { tool, inner } = splitCommandcodeRule(rule);
|
|
43393
|
+
if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
|
|
43394
|
+
category: "*",
|
|
43395
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43396
|
+
notInAllow: true
|
|
43397
|
+
} : null;
|
|
43398
|
+
const lowered = tool.toLowerCase();
|
|
43399
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
|
|
43400
|
+
tool,
|
|
43401
|
+
inner
|
|
43402
|
+
});
|
|
43403
|
+
const category = Object.hasOwn(COMMANDCODE_TOOL_TO_CATEGORY, lowered) ? COMMANDCODE_TOOL_TO_CATEGORY[lowered] : void 0;
|
|
43404
|
+
if (category === void 0) return null;
|
|
43405
|
+
return friendlyToolRule({
|
|
43406
|
+
category,
|
|
43407
|
+
inner
|
|
43408
|
+
});
|
|
43409
|
+
}
|
|
43410
|
+
/**
|
|
43411
|
+
* Whether an imported rule is a differently-cased `MCP__<server>__<tool>`
|
|
43412
|
+
* name that folded onto an exact-prefix `mcp__` category (which Command Code
|
|
43413
|
+
* matches case-sensitively, unlike the name rule it came from).
|
|
43414
|
+
*/
|
|
43415
|
+
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
43416
|
+
const { tool } = splitCommandcodeRule(rule);
|
|
43417
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
|
|
43418
|
+
}
|
|
43419
|
+
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
43420
|
+
function scopedMcpRuleTool(rule) {
|
|
43421
|
+
const { tool, inner } = splitCommandcodeRule(rule);
|
|
43422
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
|
|
43423
|
+
}
|
|
43424
|
+
/**
|
|
43425
|
+
* The string entries of a Command Code permission list. Command Code reads
|
|
43426
|
+
* each entry on its own and skips one that is not a string, so a stray
|
|
43427
|
+
* `null` beside `Shell(rm -rf *)` must not throw the deny away with it; the
|
|
43428
|
+
* skipped entries are counted in a warning because they cannot be kept.
|
|
43429
|
+
*/
|
|
43430
|
+
function stringRules({ list, action, outcome, warn }) {
|
|
43431
|
+
if (!Array.isArray(list)) return [];
|
|
43432
|
+
const rules = list.filter((entry) => typeof entry === "string");
|
|
43433
|
+
const skipped = list.length - rules.length;
|
|
43434
|
+
if (skipped > 0) warn(`Command Code permission list "${action}" holds ${skipped} ${skipped === 1 ? "entry" : "entries"} that ${skipped === 1 ? "is" : "are"} not a string, which Command Code skips; ${skipped === 1 ? "that entry was" : "those entries were"} ${outcome}.`);
|
|
43435
|
+
return rules;
|
|
43436
|
+
}
|
|
43437
|
+
/**
|
|
43438
|
+
* The canonical categories this run rebuilds — every category the canonical
|
|
43439
|
+
* config names (an empty `bash: {}` still reclaims the previous `Shell(...)`
|
|
43440
|
+
* entries, as in the Claude Code adapter) plus every rule it emits, keyed the
|
|
43441
|
+
* way an existing entry parses back (so `mcp: { github: "deny" }`, written as
|
|
43442
|
+
* `mcp__github`, claims the `mcp__github` entries of the previous run; case
|
|
43443
|
+
* variants of the friendly tool names fold together, while MCP names are
|
|
43444
|
+
* matched exactly, as Command Code does). An existing entry whose tool folds onto one of them is
|
|
43445
|
+
* rulesync's to replace, whatever list it sits in — otherwise flipping a rule
|
|
43446
|
+
* from deny to allow would leave the old deny behind and win. Every other
|
|
43447
|
+
* entry — an internal tool name rulesync cannot model, or a modeled tool the
|
|
43448
|
+
* canonical config does not mention — is the user's (Command Code writes
|
|
43449
|
+
* interactive approvals into the same lists) and is preserved verbatim.
|
|
43450
|
+
* A `deny`/`ask` entry that is reclaimed without an equally strict rule
|
|
43451
|
+
* taking its place (say a hand-written `mcp__*` deny next to a canonical
|
|
43452
|
+
* `mcp: { github: "deny" }`) is reported, so regenerating never silently
|
|
43453
|
+
* loosens what the user wrote. Mirrors `managedClaudeToolNames` in the
|
|
43454
|
+
* Claude Code adapter.
|
|
43455
|
+
*/
|
|
43456
|
+
function preservedRules({ existingPermissions, key, managedCategories, written, logger }) {
|
|
43457
|
+
return stringRules({
|
|
43458
|
+
list: existingPermissions[key],
|
|
43459
|
+
action: key,
|
|
43460
|
+
outcome: "dropped from the regenerated list",
|
|
43461
|
+
warn: (message) => logger?.warn(message)
|
|
43462
|
+
}).filter((rule) => {
|
|
43463
|
+
const parsed = parseCommandcodeRule(rule);
|
|
43464
|
+
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
43465
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
|
|
43466
|
+
if (key !== "allow" && isStricterAction({
|
|
43467
|
+
action: key,
|
|
43468
|
+
existing: replacement
|
|
43469
|
+
})) logger?.warn(`Command Code permission rule '${rule}' in "${key}" belongs to the '${parsed.category}' category, which the rulesync config now manages, and was replaced by its rules.`);
|
|
43470
|
+
return false;
|
|
43471
|
+
});
|
|
43472
|
+
}
|
|
43473
|
+
/**
|
|
43474
|
+
* The rule to write for a canonical rule, or `null` when Command Code would
|
|
43475
|
+
* ignore it in the `action` list. A server-less wildcard (`*`, `mcp__*`) in
|
|
43476
|
+
* `allow` is warned about and left unwritten rather than written dead. A
|
|
43477
|
+
* specifier on an MCP rule is ignored by Command Code in every list, so a
|
|
43478
|
+
* scoped `deny`/`ask` is written as the bare tool (which is what Command
|
|
43479
|
+
* Code would enforce anyway, only now visibly) with a warning, and a scoped
|
|
43480
|
+
* `allow` — which would grant the whole tool — is refused. Judged on the
|
|
43481
|
+
* rule as written, so the bare `mcp` category cannot smuggle a specifier in
|
|
43482
|
+
* through a `<server>__<tool>(specifier)` pattern.
|
|
43483
|
+
*/
|
|
43484
|
+
function writableCommandcodeRule({ rule, emitted, action, logger }) {
|
|
43485
|
+
if (action === "allow" && emitted.notInAllow) {
|
|
43486
|
+
logger?.warn(emitted.category === "bash" ? `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, so the '${emitted.category}' allow rule was not written; use the '*' pattern for the whole tool.` : `Command Code does not honor '${rule}' in "allow" as written (an allow rule must name what it grants), so the '${emitted.category}' allow rule was not written.`);
|
|
43487
|
+
return null;
|
|
43488
|
+
}
|
|
43489
|
+
const tool = scopedMcpRuleTool(rule);
|
|
43490
|
+
if (tool === null) return rule;
|
|
43491
|
+
if (action === "allow") {
|
|
43492
|
+
logger?.warn(`Command Code ignores the specifier of an MCP rule and would allow the whole '${tool}' tool, so the '${rule}' allow rule was not written.`);
|
|
43493
|
+
return null;
|
|
43494
|
+
}
|
|
43495
|
+
logger?.warn(`Command Code ignores the specifier of an MCP rule, so '${rule}' was written as '${tool}' and applies "${action}" to the whole tool.`);
|
|
43496
|
+
return tool;
|
|
43497
|
+
}
|
|
43498
|
+
/**
|
|
43499
|
+
* Record `action` for `rule`, keeping the stricter one (deny > ask > allow)
|
|
43500
|
+
* when two canonical rules collapse onto the same Command Code entry.
|
|
43501
|
+
*/
|
|
43502
|
+
function rankCommandcodeRule({ ranked, rule, action, logger }) {
|
|
43503
|
+
const existing = ranked.get(rule);
|
|
43504
|
+
if (existing !== void 0 && existing !== action) logger?.warn(`Command Code permission rule '${rule}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
|
|
43505
|
+
if (isStricterAction({
|
|
43506
|
+
action,
|
|
43507
|
+
existing
|
|
43508
|
+
})) ranked.set(rule, action);
|
|
43509
|
+
}
|
|
43510
|
+
/**
|
|
43511
|
+
* Bucket the canonical rules into Command Code's `allow`/`ask`/`deny` lists.
|
|
43512
|
+
* Collisions resolve to the strictest action (deny > ask > allow); categories
|
|
43513
|
+
* Command Code cannot express are skipped with a warning when they carry a
|
|
43514
|
+
* `deny`; rules Command Code would ignore in the target list are dropped or
|
|
43515
|
+
* rewritten by `writableCommandcodeRule`.
|
|
43516
|
+
*/
|
|
43517
|
+
function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
43518
|
+
const permission = honorAllToolsOnBash(config.permission);
|
|
43519
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
43520
|
+
const managedCategories = /* @__PURE__ */ new Set();
|
|
43521
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
43522
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
|
|
43523
|
+
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
43524
|
+
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
43525
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
43526
|
+
const rule = buildCommandcodeRule(category, pattern);
|
|
43527
|
+
const emitted = rule === null ? null : parseCommandcodeRule(rule);
|
|
43528
|
+
if (rule === null || emitted === null) {
|
|
43529
|
+
const honoredOnShell = category === "*" && permission.bash !== void 0;
|
|
43530
|
+
if (action === "deny" && !honoredOnShell) logger?.warn(`Command Code has no permission rule for the '${category}' category with pattern '${pattern}'; its 'deny' rule could not be represented and was skipped.`);
|
|
43531
|
+
continue;
|
|
43532
|
+
}
|
|
43533
|
+
managedCategories.add(emitted.category);
|
|
43534
|
+
const written = writableCommandcodeRule({
|
|
43535
|
+
rule,
|
|
43536
|
+
emitted,
|
|
43537
|
+
action,
|
|
43538
|
+
logger
|
|
43539
|
+
});
|
|
43540
|
+
if (written !== null) rankCommandcodeRule({
|
|
43541
|
+
ranked,
|
|
43542
|
+
rule: written,
|
|
43543
|
+
action,
|
|
43544
|
+
logger
|
|
43545
|
+
});
|
|
43546
|
+
}
|
|
43547
|
+
}
|
|
43548
|
+
const written = /* @__PURE__ */ new Map();
|
|
43549
|
+
for (const [rule, action] of ranked) {
|
|
43550
|
+
const parsed = parseCommandcodeRule(rule);
|
|
43551
|
+
if (parsed !== null) written.set(`${parsed.category}(${parsed.pattern})`, action);
|
|
43552
|
+
}
|
|
43553
|
+
const preserved = {
|
|
43554
|
+
existingPermissions,
|
|
43555
|
+
managedCategories,
|
|
43556
|
+
written,
|
|
43557
|
+
logger
|
|
43558
|
+
};
|
|
43559
|
+
const allow = preservedRules({
|
|
43560
|
+
...preserved,
|
|
43561
|
+
key: "allow"
|
|
43562
|
+
});
|
|
43563
|
+
const ask = preservedRules({
|
|
43564
|
+
...preserved,
|
|
43565
|
+
key: "ask"
|
|
43566
|
+
});
|
|
43567
|
+
const deny = preservedRules({
|
|
43568
|
+
...preserved,
|
|
43569
|
+
key: "deny"
|
|
43570
|
+
});
|
|
43571
|
+
for (const [rule, action] of ranked) if (action === "allow") allow.push(rule);
|
|
43572
|
+
else if (action === "ask") ask.push(rule);
|
|
43573
|
+
else deny.push(rule);
|
|
43574
|
+
return {
|
|
43575
|
+
allow: (0, es_toolkit.uniq)(allow.toSorted()),
|
|
43576
|
+
ask: (0, es_toolkit.uniq)(ask.toSorted()),
|
|
43577
|
+
deny: (0, es_toolkit.uniq)(deny.toSorted())
|
|
43578
|
+
};
|
|
43579
|
+
}
|
|
43580
|
+
/**
|
|
43581
|
+
* Why a rule marked `notInAllow` is not imported from `allow`: Command Code
|
|
43582
|
+
* ignores a server-less wildcard or a tool-name glob there, while it does
|
|
43583
|
+
* enforce a scoped `MCP__<server>__<tool>(specifier)` — as a glob over the
|
|
43584
|
+
* call's arguments — and a padded `Shell( * )` — as a pattern narrower than
|
|
43585
|
+
* the bare `Shell` — which rulesync cannot model in `allow` without widening
|
|
43586
|
+
* the grant.
|
|
43587
|
+
*/
|
|
43588
|
+
function skippedAllowImportMessage({ rule, category }) {
|
|
43589
|
+
const { tool, inner } = splitCommandcodeRule(rule);
|
|
43590
|
+
if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
|
|
43591
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$5) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
|
|
43592
|
+
return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
|
|
43593
|
+
}
|
|
43594
|
+
/**
|
|
43595
|
+
* Parse Command Code's `permissions` lists back into a canonical permission
|
|
43596
|
+
* map with `deny > ask > allow` precedence, so a tool described more than
|
|
43597
|
+
* once resolves to the strictest action. Each rule is imported as what
|
|
43598
|
+
* Command Code enforces for it, never as what it looks like: a rule Command
|
|
43599
|
+
* Code ignores in `allow` (`*`, `mcp__*`, an `MCP__...` glob) is skipped
|
|
43600
|
+
* there with a warning, since importing it would turn a dead line into a
|
|
43601
|
+
* live grant for every other target, and so is a scoped
|
|
43602
|
+
* `MCP__<server>__<tool>(specifier)` allow, which Command Code enforces as a
|
|
43603
|
+
* glob over the call's arguments but rulesync cannot import without widening
|
|
43604
|
+
* it to the whole tool; the specifier of an exact-prefix `mcp__...` rule,
|
|
43605
|
+
* which Command Code drops, is folded to the whole tool in every list — with
|
|
43606
|
+
* a warning in `allow`, where the fold widens the grant the user wrote. A
|
|
43607
|
+
* tool-half glob in that exact-prefix spelling (`mcp__<server>__get_*`), which
|
|
43608
|
+
* Command Code matches against tool names in every list, is imported as the
|
|
43609
|
+
* category spelled that way, the same one the generator writes back; other
|
|
43610
|
+
* targets read it as a literal tool name.
|
|
43611
|
+
*/
|
|
43612
|
+
function parseCommandcodeRuleLists(permissions) {
|
|
43613
|
+
const permission = {};
|
|
43614
|
+
const lists = [
|
|
43615
|
+
["allow", permissions.allow],
|
|
43616
|
+
["ask", permissions.ask],
|
|
43617
|
+
["deny", permissions.deny]
|
|
43618
|
+
];
|
|
43619
|
+
for (const [action, rawList] of lists) {
|
|
43620
|
+
const list = stringRules({
|
|
43621
|
+
list: rawList,
|
|
43622
|
+
action,
|
|
43623
|
+
outcome: "not imported",
|
|
43624
|
+
warn: (message) => fallbackLogger.warn(message)
|
|
43625
|
+
});
|
|
43626
|
+
for (const rule of list) {
|
|
43627
|
+
const parsed = parseCommandcodeRule(rule);
|
|
43628
|
+
if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) {
|
|
43629
|
+
if (action !== "allow") fallbackLogger.warn(`Command Code permission rule '${rule}' in "${action}" is not one rulesync can model, so it could not be imported; it stays in the Command Code settings, where a regenerate keeps it unless the canonical config manages its category.`);
|
|
43630
|
+
continue;
|
|
43631
|
+
}
|
|
43632
|
+
const { category, pattern } = parsed;
|
|
43633
|
+
if (action === "allow" && parsed.notInAllow) {
|
|
43634
|
+
fallbackLogger.warn(skippedAllowImportMessage({
|
|
43635
|
+
rule,
|
|
43636
|
+
category
|
|
43637
|
+
}));
|
|
43638
|
+
continue;
|
|
43639
|
+
}
|
|
43640
|
+
if (action === "allow" && scopedMcpRuleTool(rule) !== null) fallbackLogger.warn(`Command Code ignores the specifier of an MCP rule, so '${rule}' in "allow" was imported as the whole '${category}' tool, which is what Command Code grants for it.`);
|
|
43641
|
+
if (isFoldedMcpToolNameRule({
|
|
43642
|
+
rule,
|
|
43643
|
+
category
|
|
43644
|
+
})) fallbackLogger.warn(`Command Code matches '${rule}' in "${action}" against MCP tool names case-insensitively, but the '${category}' rule generated from it matches the server and tool names exactly as written; spell the rule the way the tool is registered if they differ.`);
|
|
43645
|
+
const rules = permission[category] ??= {};
|
|
43646
|
+
if (isStricterAction({
|
|
43647
|
+
action,
|
|
43648
|
+
existing: rules[pattern]
|
|
43649
|
+
})) rules[pattern] = action;
|
|
43650
|
+
}
|
|
43651
|
+
}
|
|
43652
|
+
return permission;
|
|
43653
|
+
}
|
|
43654
|
+
/**
|
|
43655
|
+
* Permissions generator for Command Code.
|
|
43656
|
+
*
|
|
43657
|
+
* Rules live under the `permissions` key of `.commandcode/settings.json`
|
|
43658
|
+
* (project) and `~/.commandcode/settings.json` (user) as three lists of
|
|
43659
|
+
* Claude-style entries: `deny` (always wins), `ask`, `allow`. The same key
|
|
43660
|
+
* also carries `defaultMode`, `additionalDirectories` and `disableBypass`,
|
|
43661
|
+
* and the file holds `hooks` and other settings, so writes go through the
|
|
43662
|
+
* shared-config gateway: rulesync replaces the three lists, keeps every
|
|
43663
|
+
* sibling key, and never deletes the file.
|
|
43664
|
+
*
|
|
43665
|
+
* Generate: `permission.<category>.<pattern>` becomes `Tool(pattern)` with
|
|
43666
|
+
* the friendly names (`Shell`, `Read`, `Edit`, `Write`, `WebFetch`,
|
|
43667
|
+
* `WebSearch`), `mcp__<server>__<tool>` names pass through, and the all-tools
|
|
43668
|
+
* `*` category becomes the bare `*` rule Command Code accepts in `deny`/`ask`.
|
|
43669
|
+
* Only the entries of the categories the canonical config names are rebuilt;
|
|
43670
|
+
* every other existing entry is preserved verbatim.
|
|
43671
|
+
* Import: the lists are parsed back as what Command Code enforces for each
|
|
43672
|
+
* entry — friendly tool names case-insensitively (with the internal aliases
|
|
43673
|
+
* such as `Bash`, `PowerShell` and `write_file` folding onto their
|
|
43674
|
+
* category), MCP names exactly; rules for internal tool names rulesync does
|
|
43675
|
+
* not model are skipped.
|
|
43676
|
+
*
|
|
43677
|
+
* @see https://commandcode.ai/docs/permissions
|
|
43678
|
+
* @see https://commandcode.ai/docs/settings
|
|
43679
|
+
*/
|
|
43680
|
+
var CommandcodePermissions = class CommandcodePermissions extends ToolPermissions {
|
|
43681
|
+
constructor(params) {
|
|
43682
|
+
super({
|
|
43683
|
+
...params,
|
|
43684
|
+
fileContent: params.fileContent ?? "{}"
|
|
43685
|
+
});
|
|
43686
|
+
}
|
|
43687
|
+
/**
|
|
43688
|
+
* `settings.json` holds hooks and other user settings, so it is never
|
|
43689
|
+
* deleted; clearing permissions happens via an in-place merge.
|
|
43690
|
+
*/
|
|
43691
|
+
isDeletable() {
|
|
43692
|
+
return false;
|
|
43693
|
+
}
|
|
43694
|
+
static getSettablePaths(_options = {}) {
|
|
43695
|
+
return {
|
|
43696
|
+
relativeDirPath: COMMANDCODE_DIR,
|
|
43697
|
+
relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
|
|
43698
|
+
};
|
|
43699
|
+
}
|
|
43700
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
43701
|
+
const paths = CommandcodePermissions.getSettablePaths({ global });
|
|
43702
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
43703
|
+
return new CommandcodePermissions({
|
|
43704
|
+
outputRoot,
|
|
43705
|
+
relativeDirPath: paths.relativeDirPath,
|
|
43706
|
+
relativeFilePath: paths.relativeFilePath,
|
|
43707
|
+
fileContent,
|
|
43708
|
+
validate,
|
|
43709
|
+
global
|
|
43710
|
+
});
|
|
43711
|
+
}
|
|
43712
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
|
|
43713
|
+
const paths = CommandcodePermissions.getSettablePaths({ global });
|
|
43714
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43715
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
43716
|
+
const existing = parseCommandcodeSettings({
|
|
43717
|
+
fileContent: existingContent,
|
|
43718
|
+
relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
|
|
43719
|
+
});
|
|
43720
|
+
const existingPermissions = isRecord$1(existing[COMMANDCODE_PERMISSIONS_KEY]) ? existing[COMMANDCODE_PERMISSIONS_KEY] : {};
|
|
43721
|
+
const lists = buildCommandcodeRuleLists({
|
|
43722
|
+
config: rulesyncPermissions.getJson(),
|
|
43723
|
+
existingPermissions,
|
|
43724
|
+
logger
|
|
43725
|
+
});
|
|
43726
|
+
const permissions = { ...existingPermissions };
|
|
43727
|
+
for (const key of [
|
|
43728
|
+
"allow",
|
|
43729
|
+
"ask",
|
|
43730
|
+
"deny"
|
|
43731
|
+
]) if (lists[key].length > 0) permissions[key] = lists[key];
|
|
43732
|
+
else delete permissions[key];
|
|
43733
|
+
return new CommandcodePermissions({
|
|
43734
|
+
outputRoot,
|
|
43735
|
+
relativeDirPath: paths.relativeDirPath,
|
|
43736
|
+
relativeFilePath: paths.relativeFilePath,
|
|
43737
|
+
fileContent: applySharedConfigPatch({
|
|
43738
|
+
fileKey: sharedConfigFileKey(paths),
|
|
43739
|
+
feature: "permissions",
|
|
43740
|
+
existingContent,
|
|
43741
|
+
patch: { [COMMANDCODE_PERMISSIONS_KEY]: permissions },
|
|
43742
|
+
filePath
|
|
43743
|
+
}),
|
|
43744
|
+
validate: true,
|
|
43745
|
+
global
|
|
43746
|
+
});
|
|
43747
|
+
}
|
|
43748
|
+
toRulesyncPermissions() {
|
|
43749
|
+
const settings = parseCommandcodeSettings({
|
|
43750
|
+
fileContent: this.getFileContent() || "{}",
|
|
43751
|
+
relativePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
43752
|
+
});
|
|
43753
|
+
const permissions = isRecord$1(settings[COMMANDCODE_PERMISSIONS_KEY]) ? settings[COMMANDCODE_PERMISSIONS_KEY] : {};
|
|
43754
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission: parseCommandcodeRuleLists(permissions) }, null, 2) });
|
|
43755
|
+
}
|
|
43756
|
+
validate() {
|
|
43757
|
+
return {
|
|
43758
|
+
success: true,
|
|
43759
|
+
error: null
|
|
43760
|
+
};
|
|
43761
|
+
}
|
|
43762
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
43763
|
+
return new CommandcodePermissions({
|
|
43764
|
+
outputRoot,
|
|
43765
|
+
relativeDirPath,
|
|
43766
|
+
relativeFilePath,
|
|
43767
|
+
fileContent: "{}",
|
|
43768
|
+
validate: false,
|
|
43769
|
+
global
|
|
43770
|
+
});
|
|
43771
|
+
}
|
|
43772
|
+
};
|
|
43773
|
+
/**
|
|
43774
|
+
* Fail closed on a syntax error or non-object root, matching the write path's
|
|
43775
|
+
* shared-config declaration, so a broken file is surfaced rather than
|
|
43776
|
+
* partially imported or overwritten.
|
|
43777
|
+
*/
|
|
43778
|
+
function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
43779
|
+
try {
|
|
43780
|
+
return parseSharedConfig({
|
|
43781
|
+
format: "json",
|
|
43782
|
+
fileContent,
|
|
43783
|
+
filePath: relativePath,
|
|
43784
|
+
invalidRootPolicy: "error"
|
|
43785
|
+
});
|
|
43786
|
+
} catch (error) {
|
|
43787
|
+
throw new Error(`Failed to parse Command Code settings in ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
43788
|
+
}
|
|
43789
|
+
}
|
|
43790
|
+
//#endregion
|
|
41971
43791
|
//#region src/features/permissions/continue-permissions.ts
|
|
41972
43792
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
41973
|
-
const CATCH_ALL_PATTERN$
|
|
43793
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
43794
|
+
/**
|
|
43795
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
43796
|
+
* would match `Tool(**)` against every argument just as the bare tool name
|
|
43797
|
+
* does, so it is written — and, under the all-tools category, honored — as
|
|
43798
|
+
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
43799
|
+
*/
|
|
43800
|
+
function normalizeCatchAllPattern$1(pattern) {
|
|
43801
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
43802
|
+
}
|
|
41974
43803
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
41975
43804
|
bash: "Bash",
|
|
41976
43805
|
read: "Read",
|
|
@@ -41998,7 +43827,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
41998
43827
|
function toContinueToolName(canonical) {
|
|
41999
43828
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
42000
43829
|
}
|
|
42001
|
-
function toCanonicalToolName$
|
|
43830
|
+
function toCanonicalToolName$5(continueName) {
|
|
42002
43831
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
42003
43832
|
}
|
|
42004
43833
|
/**
|
|
@@ -42006,7 +43835,7 @@ function toCanonicalToolName$4(continueName) {
|
|
|
42006
43835
|
* `Tool(pattern)`.
|
|
42007
43836
|
*/
|
|
42008
43837
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
42009
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
43838
|
+
return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
|
|
42010
43839
|
}
|
|
42011
43840
|
/**
|
|
42012
43841
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -42020,7 +43849,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
42020
43849
|
if (toolName === "") return;
|
|
42021
43850
|
return {
|
|
42022
43851
|
toolName,
|
|
42023
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
43852
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
|
|
42024
43853
|
};
|
|
42025
43854
|
}
|
|
42026
43855
|
/**
|
|
@@ -42187,13 +44016,14 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
42187
44016
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42188
44017
|
if (isPrototypePollutionKey(category)) continue;
|
|
42189
44018
|
const toolName = category === "*" ? "*" : toContinueToolName(category);
|
|
42190
|
-
for (const [
|
|
42191
|
-
if (isPrototypePollutionKey(
|
|
44019
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
44020
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
44021
|
+
const pattern = normalizeCatchAllPattern$1(rawPattern);
|
|
42192
44022
|
if (pattern.includes("(") || pattern.includes(")")) {
|
|
42193
44023
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42194
44024
|
continue;
|
|
42195
44025
|
}
|
|
42196
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
44026
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
|
|
42197
44027
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42198
44028
|
continue;
|
|
42199
44029
|
}
|
|
@@ -42218,7 +44048,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
42218
44048
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
42219
44049
|
if (parsedEntry === void 0) continue;
|
|
42220
44050
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
42221
|
-
const category = toCanonicalToolName$
|
|
44051
|
+
const category = toCanonicalToolName$5(parsedEntry.toolName);
|
|
42222
44052
|
permission[category] ??= {};
|
|
42223
44053
|
permission[category][parsedEntry.pattern] ??= action;
|
|
42224
44054
|
}
|
|
@@ -42541,6 +44371,295 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
42541
44371
|
}
|
|
42542
44372
|
};
|
|
42543
44373
|
//#endregion
|
|
44374
|
+
//#region src/features/permissions/crush-permissions.ts
|
|
44375
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
44376
|
+
/**
|
|
44377
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
44378
|
+
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
44379
|
+
* still be written.
|
|
44380
|
+
*/
|
|
44381
|
+
function normalizeCatchAllPattern(pattern) {
|
|
44382
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
|
|
44383
|
+
}
|
|
44384
|
+
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
44385
|
+
bash: "bash",
|
|
44386
|
+
read: "view",
|
|
44387
|
+
edit: "edit",
|
|
44388
|
+
write: "write",
|
|
44389
|
+
grep: "grep",
|
|
44390
|
+
glob: "glob",
|
|
44391
|
+
webfetch: "fetch"
|
|
44392
|
+
};
|
|
44393
|
+
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
44394
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
44395
|
+
const MCP_CRUSH_PREFIX = "mcp_";
|
|
44396
|
+
function toCrushToolName(canonical) {
|
|
44397
|
+
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
44398
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
44399
|
+
return canonical;
|
|
44400
|
+
}
|
|
44401
|
+
function toCanonicalToolName$4(crushName) {
|
|
44402
|
+
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
44403
|
+
}
|
|
44404
|
+
function isMcpToolName(crushName) {
|
|
44405
|
+
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
44406
|
+
}
|
|
44407
|
+
function uniq$9(values) {
|
|
44408
|
+
return [...new Set(values)];
|
|
44409
|
+
}
|
|
44410
|
+
/**
|
|
44411
|
+
* Permissions adapter for Crush.
|
|
44412
|
+
*
|
|
44413
|
+
* Crush has no per-argument permission rules. Its JSON config carries two
|
|
44414
|
+
* tool-level lists instead:
|
|
44415
|
+
* - `permissions.allowed_tools`: tools (or `tool:action` pairs) that run
|
|
44416
|
+
* without a permission prompt; everything else prompts.
|
|
44417
|
+
* - `options.disabled_tools`: built-in tools removed from the agent
|
|
44418
|
+
* entirely.
|
|
44419
|
+
*
|
|
44420
|
+
* Mapping (rulesync canonical -> Crush), catch-all (`*`) rules only:
|
|
44421
|
+
* - `allow` -> an `allowed_tools` entry.
|
|
44422
|
+
* - `deny` -> a `disabled_tools` entry. Crush only filters its built-ins
|
|
44423
|
+
* through that list, so a deny for an MCP tool is reported and skipped
|
|
44424
|
+
* (disable the tool in the MCP server's `disabled_tools` instead).
|
|
44425
|
+
* - `ask` -> nothing (prompting is Crush's default).
|
|
44426
|
+
* - Tool name: `bash` -> `bash`, `read` -> `view`, `edit` -> `edit`,
|
|
44427
|
+
* `write` -> `write`, `grep` -> `grep`, `glob` -> `glob`,
|
|
44428
|
+
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
44429
|
+
* any other category passes through verbatim.
|
|
44430
|
+
*
|
|
44431
|
+
* A pattern-specific rule cannot be expressed and is reported and skipped.
|
|
44432
|
+
* Because Crush cannot narrow an allowed tool, a category's catch-all `allow`
|
|
44433
|
+
* is not written while the same category carries a pattern-specific `deny` or
|
|
44434
|
+
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
44435
|
+
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
44436
|
+
* `honorAllToolsOnBash` and otherwise skipped, since Crush has no allow-all
|
|
44437
|
+
* or disable-all list.
|
|
44438
|
+
*
|
|
44439
|
+
* `crush.json` is Crush's main config: entries naming a tool the canonical
|
|
44440
|
+
* config does not manage, and every `tool:action` entry (a form rulesync
|
|
44441
|
+
* never derives), are preserved verbatim; the managed tools' bare entries
|
|
44442
|
+
* are rebuilt, every other key of `permissions` / `options` and of the file is
|
|
44443
|
+
* kept, and the file is never deleted. Project scope writes `crush.json`, or
|
|
44444
|
+
* an existing `.crush.json`; Crush merges the pair (lists concatenated), so an
|
|
44445
|
+
* entry left in the other file stays in effect and is reported.
|
|
44446
|
+
*
|
|
44447
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
44448
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
44449
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/permission/permission.go
|
|
44450
|
+
*/
|
|
44451
|
+
var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
44452
|
+
json;
|
|
44453
|
+
constructor(params) {
|
|
44454
|
+
super({
|
|
44455
|
+
...params,
|
|
44456
|
+
fileContent: params.fileContent ?? ""
|
|
44457
|
+
});
|
|
44458
|
+
this.json = parseCrushConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
44459
|
+
}
|
|
44460
|
+
getJson() {
|
|
44461
|
+
return this.json;
|
|
44462
|
+
}
|
|
44463
|
+
isDeletable() {
|
|
44464
|
+
return false;
|
|
44465
|
+
}
|
|
44466
|
+
/**
|
|
44467
|
+
* `crush.json` is Crush's file: rulesync merges into it when it exists but
|
|
44468
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
44469
|
+
* absent `allowed_tools` both mean "prompt for everything".
|
|
44470
|
+
*/
|
|
44471
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
44472
|
+
return true;
|
|
44473
|
+
}
|
|
44474
|
+
static getSettablePaths({ global = false } = {}) {
|
|
44475
|
+
return getCrushConfigSettablePaths({ global });
|
|
44476
|
+
}
|
|
44477
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
44478
|
+
const location = await resolveCrushConfigFile({
|
|
44479
|
+
outputRoot,
|
|
44480
|
+
global
|
|
44481
|
+
});
|
|
44482
|
+
return new CrushPermissions({
|
|
44483
|
+
outputRoot,
|
|
44484
|
+
relativeDirPath: location.relativeDirPath,
|
|
44485
|
+
relativeFilePath: location.relativeFilePath,
|
|
44486
|
+
fileContent: crushConfigImportContent(location),
|
|
44487
|
+
validate,
|
|
44488
|
+
global
|
|
44489
|
+
});
|
|
44490
|
+
}
|
|
44491
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
44492
|
+
const location = await resolveCrushConfigFile({
|
|
44493
|
+
outputRoot,
|
|
44494
|
+
global
|
|
44495
|
+
});
|
|
44496
|
+
const existingContent = location.fileContent ?? "";
|
|
44497
|
+
warnCrushTwinLeftovers({
|
|
44498
|
+
location,
|
|
44499
|
+
ownedPaths: [[CRUSH_PERMISSIONS_KEY, CRUSH_ALLOWED_TOOLS_KEY], [CRUSH_OPTIONS_KEY, CRUSH_DISABLED_TOOLS_KEY]],
|
|
44500
|
+
logger
|
|
44501
|
+
});
|
|
44502
|
+
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
44503
|
+
const existingPermissions = isRecord$1(existing["permissions"]) ? existing[CRUSH_PERMISSIONS_KEY] : void 0;
|
|
44504
|
+
const existingOptions = isRecord$1(existing["options"]) ? existing[CRUSH_OPTIONS_KEY] : void 0;
|
|
44505
|
+
const config = rulesyncPermissions.getJson();
|
|
44506
|
+
const generated = convertRulesyncToCrushLists({
|
|
44507
|
+
config,
|
|
44508
|
+
logger
|
|
44509
|
+
});
|
|
44510
|
+
const managedToolNames = managedCrushToolNames(config);
|
|
44511
|
+
const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
44512
|
+
const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
44513
|
+
const allowedList = uniq$9([...preservedAllowed, ...generated.allowed]);
|
|
44514
|
+
const disabledList = uniq$9([...preservedDisabled, ...generated.disabled]);
|
|
44515
|
+
const patch = {};
|
|
44516
|
+
if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
|
|
44517
|
+
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
44518
|
+
return new CrushPermissions({
|
|
44519
|
+
outputRoot,
|
|
44520
|
+
relativeDirPath: location.relativeDirPath,
|
|
44521
|
+
relativeFilePath: location.relativeFilePath,
|
|
44522
|
+
fileContent: applySharedConfigPatch({
|
|
44523
|
+
fileKey: sharedConfigFileKey(CrushPermissions.getSettablePaths({ global })),
|
|
44524
|
+
feature: "permissions",
|
|
44525
|
+
existingContent,
|
|
44526
|
+
patch,
|
|
44527
|
+
filePath: location.filePath,
|
|
44528
|
+
logger
|
|
44529
|
+
}),
|
|
44530
|
+
validate: true,
|
|
44531
|
+
global
|
|
44532
|
+
});
|
|
44533
|
+
}
|
|
44534
|
+
toRulesyncPermissions() {
|
|
44535
|
+
const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
44536
|
+
const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
|
|
44537
|
+
const rulesyncConfig = convertCrushListsToRulesync({
|
|
44538
|
+
allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
|
|
44539
|
+
disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
|
|
44540
|
+
});
|
|
44541
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
44542
|
+
}
|
|
44543
|
+
validate() {
|
|
44544
|
+
return {
|
|
44545
|
+
success: true,
|
|
44546
|
+
error: null
|
|
44547
|
+
};
|
|
44548
|
+
}
|
|
44549
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
44550
|
+
return new CrushPermissions({
|
|
44551
|
+
outputRoot,
|
|
44552
|
+
relativeDirPath,
|
|
44553
|
+
relativeFilePath,
|
|
44554
|
+
fileContent: "{}",
|
|
44555
|
+
validate: false,
|
|
44556
|
+
global
|
|
44557
|
+
});
|
|
44558
|
+
}
|
|
44559
|
+
};
|
|
44560
|
+
/**
|
|
44561
|
+
* The Crush tool names the canonical config manages, i.e. the names its
|
|
44562
|
+
* categories map to. Entries naming any other tool are the user's and survive
|
|
44563
|
+
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
44564
|
+
*/
|
|
44565
|
+
function managedCrushToolNames(config) {
|
|
44566
|
+
return new Set(Object.keys(config.permission).filter((category) => category !== "*" && !isPrototypePollutionKey(category)).map((category) => toCrushToolName(category)));
|
|
44567
|
+
}
|
|
44568
|
+
/**
|
|
44569
|
+
* Reduce one category's rules to the single tool-wide action Crush can carry,
|
|
44570
|
+
* or undefined when nothing should be written for the tool: no catch-all rule,
|
|
44571
|
+
* a catch-all allow next to a narrower deny/ask (Crush cannot narrow an
|
|
44572
|
+
* allowed tool, so it keeps prompting instead), or a deny on an MCP tool
|
|
44573
|
+
* (`options.disabled_tools` only covers built-ins).
|
|
44574
|
+
*/
|
|
44575
|
+
function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
44576
|
+
let catchAllAction;
|
|
44577
|
+
let restricted = false;
|
|
44578
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
44579
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
44580
|
+
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
44581
|
+
if (pattern === CATCH_ALL_PATTERN$4) {
|
|
44582
|
+
catchAllAction = action;
|
|
44583
|
+
continue;
|
|
44584
|
+
}
|
|
44585
|
+
logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
44586
|
+
if (action !== "allow") restricted = true;
|
|
44587
|
+
}
|
|
44588
|
+
if (catchAllAction === "allow" && restricted) {
|
|
44589
|
+
logger?.warn(`Crush cannot narrow an allowed tool, so "${category}" is not added to permissions.allowed_tools while it carries a pattern-specific deny/ask rule; Crush keeps prompting for "${toolName}".`);
|
|
44590
|
+
return;
|
|
44591
|
+
}
|
|
44592
|
+
if (catchAllAction === "deny" && isMcpToolName(toolName)) {
|
|
44593
|
+
logger?.warn(`Crush's options.disabled_tools only covers built-in tools, so the "deny" rule for "${category}" was skipped; disable the tool through the MCP server's "disabled_tools" instead.`);
|
|
44594
|
+
return;
|
|
44595
|
+
}
|
|
44596
|
+
return catchAllAction;
|
|
44597
|
+
}
|
|
44598
|
+
/**
|
|
44599
|
+
* Convert a rulesync permissions config into Crush's two tool lists.
|
|
44600
|
+
*/
|
|
44601
|
+
function convertRulesyncToCrushLists({ config, logger }) {
|
|
44602
|
+
const lists = {
|
|
44603
|
+
allowed: [],
|
|
44604
|
+
disabled: []
|
|
44605
|
+
};
|
|
44606
|
+
const actionByTool = /* @__PURE__ */ new Map();
|
|
44607
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
44608
|
+
if (isPrototypePollutionKey(category)) continue;
|
|
44609
|
+
if (category === "*") {
|
|
44610
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
44611
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
44612
|
+
logger?.warn(`Crush has no allow-all or disable-all tool list, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
44613
|
+
}
|
|
44614
|
+
continue;
|
|
44615
|
+
}
|
|
44616
|
+
const toolName = toCrushToolName(category);
|
|
44617
|
+
const catchAllAction = resolveCategoryAction({
|
|
44618
|
+
category,
|
|
44619
|
+
toolName,
|
|
44620
|
+
rules,
|
|
44621
|
+
logger
|
|
44622
|
+
});
|
|
44623
|
+
if (catchAllAction === void 0) continue;
|
|
44624
|
+
const previous = actionByTool.get(toolName);
|
|
44625
|
+
if (previous !== void 0 && previous !== catchAllAction) logger?.warn(`Crush permissions: rules from different categories both resolve to ${quoteValueForWarning(toolName)} with conflicting actions (${previous} and ${catchAllAction}). Both are written; a disabled tool is hidden from the agent regardless of allowed_tools.`);
|
|
44626
|
+
actionByTool.set(toolName, catchAllAction);
|
|
44627
|
+
if (catchAllAction === "allow") lists.allowed.push(toolName);
|
|
44628
|
+
else if (catchAllAction === "deny") lists.disabled.push(toolName);
|
|
44629
|
+
}
|
|
44630
|
+
return {
|
|
44631
|
+
allowed: uniq$9(lists.allowed),
|
|
44632
|
+
disabled: uniq$9(lists.disabled)
|
|
44633
|
+
};
|
|
44634
|
+
}
|
|
44635
|
+
/**
|
|
44636
|
+
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
44637
|
+
* entry scopes an allow to one action of a tool, which the canonical model
|
|
44638
|
+
* cannot express, so it is left out rather than widened to the whole tool.
|
|
44639
|
+
*/
|
|
44640
|
+
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
44641
|
+
const permission = {};
|
|
44642
|
+
const bucketFor = (category) => {
|
|
44643
|
+
const own = lookupOwn({
|
|
44644
|
+
record: permission,
|
|
44645
|
+
key: category
|
|
44646
|
+
});
|
|
44647
|
+
if (own !== void 0) return own;
|
|
44648
|
+
const created = {};
|
|
44649
|
+
permission[category] = created;
|
|
44650
|
+
return created;
|
|
44651
|
+
};
|
|
44652
|
+
for (const entry of disabled) {
|
|
44653
|
+
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
44654
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
|
|
44655
|
+
}
|
|
44656
|
+
for (const entry of allowed) {
|
|
44657
|
+
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
44658
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
|
|
44659
|
+
}
|
|
44660
|
+
return { permission };
|
|
44661
|
+
}
|
|
44662
|
+
//#endregion
|
|
42544
44663
|
//#region src/features/permissions/cursor-permissions.ts
|
|
42545
44664
|
/**
|
|
42546
44665
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -43695,6 +45814,40 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43695
45814
|
return Object.keys(bash).length > 0 ? { permission: { bash } } : { permission: {} };
|
|
43696
45815
|
}
|
|
43697
45816
|
//#endregion
|
|
45817
|
+
//#region src/features/permissions/single-action-collapse.ts
|
|
45818
|
+
/**
|
|
45819
|
+
* Strictness order shared by the tools whose permission model evaluates
|
|
45820
|
+
* `deny > ask > allow`. Used wherever several canonical rules have to fold
|
|
45821
|
+
* into one entry, so the strictest action always wins.
|
|
45822
|
+
*/
|
|
45823
|
+
const PERMISSION_ACTION_PRIORITY = {
|
|
45824
|
+
allow: 0,
|
|
45825
|
+
ask: 1,
|
|
45826
|
+
deny: 2
|
|
45827
|
+
};
|
|
45828
|
+
/**
|
|
45829
|
+
* Collapse a pattern map to the single action a tool can hold for a scope
|
|
45830
|
+
* that has no pattern matcher, using deny > ask > allow precedence.
|
|
45831
|
+
*
|
|
45832
|
+
* A map without a catch-all grants nothing to unmatched inputs, so an
|
|
45833
|
+
* implicit `ask` joins the candidates and a narrow allowlist can never widen
|
|
45834
|
+
* into a blanket allow. Returns `undefined` for an empty map; each caller
|
|
45835
|
+
* decides what an empty map means for its tool (e.g. emit nothing, or fall
|
|
45836
|
+
* back to `deny` when the tool's own default is allow).
|
|
45837
|
+
*/
|
|
45838
|
+
function collapseRulesToSingleAction({ rules }) {
|
|
45839
|
+
const actions = Object.values(rules);
|
|
45840
|
+
if (actions.length === 0) return;
|
|
45841
|
+
return (Object.hasOwn(rules, "*") ? actions : [...actions, "ask"]).reduce((current, candidate) => PERMISSION_ACTION_PRIORITY[candidate] > PERMISSION_ACTION_PRIORITY[current] ? candidate : current);
|
|
45842
|
+
}
|
|
45843
|
+
/**
|
|
45844
|
+
* Whether a pattern map carries anything beyond the `*` catch-all, i.e.
|
|
45845
|
+
* whether collapsing it to a single action loses information worth a warning.
|
|
45846
|
+
*/
|
|
45847
|
+
function hasPatternSpecificRules(rules) {
|
|
45848
|
+
return Object.keys(rules).some((pattern) => pattern !== "*");
|
|
45849
|
+
}
|
|
45850
|
+
//#endregion
|
|
43698
45851
|
//#region src/features/permissions/devin-permissions.ts
|
|
43699
45852
|
/**
|
|
43700
45853
|
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
@@ -43704,17 +45857,21 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43704
45857
|
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
43705
45858
|
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
43706
45859
|
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
43707
|
-
* `edit` rules round-trip as `write` (a lossy but documented collapse).
|
|
45860
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). The
|
|
45861
|
+
* canonical `websearch` category maps onto the bare `web_search` tool name,
|
|
45862
|
+
* accepted in the permission lists since CLI v3000.10.21 (2026-09-10). Unknown
|
|
43708
45863
|
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
43709
45864
|
*
|
|
43710
45865
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
45866
|
+
* @see https://docs.devin.ai/cli/changelog/stable — v3000.10.21, `web_search`
|
|
43711
45867
|
*/
|
|
43712
45868
|
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
43713
45869
|
read: "Read",
|
|
43714
45870
|
write: "Write",
|
|
43715
45871
|
edit: "Write",
|
|
43716
45872
|
bash: "Exec",
|
|
43717
|
-
webfetch: "Fetch"
|
|
45873
|
+
webfetch: "Fetch",
|
|
45874
|
+
websearch: "web_search"
|
|
43718
45875
|
};
|
|
43719
45876
|
/**
|
|
43720
45877
|
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
@@ -43723,8 +45880,15 @@ const DEVIN_SCOPE_TO_CANONICAL = {
|
|
|
43723
45880
|
Read: "read",
|
|
43724
45881
|
Write: "write",
|
|
43725
45882
|
Exec: "bash",
|
|
43726
|
-
Fetch: "webfetch"
|
|
45883
|
+
Fetch: "webfetch",
|
|
45884
|
+
web_search: "websearch"
|
|
43727
45885
|
};
|
|
45886
|
+
/**
|
|
45887
|
+
* Devin scopes that exist only as a bare tool name: there is no
|
|
45888
|
+
* `web_search(pattern)` matcher, so a pattern-specific rule under the canonical
|
|
45889
|
+
* category cannot be expressed and is collapsed to one action instead.
|
|
45890
|
+
*/
|
|
45891
|
+
const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
|
|
43728
45892
|
function toDevinScope(canonical) {
|
|
43729
45893
|
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
43730
45894
|
}
|
|
@@ -43909,7 +46073,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43909
46073
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
43910
46074
|
}
|
|
43911
46075
|
const config = rulesyncPermissions.getJson();
|
|
43912
|
-
const { allow, ask, deny } = convertRulesyncToDevinPermissions(
|
|
46076
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions({
|
|
46077
|
+
config,
|
|
46078
|
+
logger
|
|
46079
|
+
});
|
|
43913
46080
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
43914
46081
|
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
43915
46082
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
@@ -44013,26 +46180,50 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
44013
46180
|
}
|
|
44014
46181
|
};
|
|
44015
46182
|
/**
|
|
46183
|
+
* Collapse the pattern rules of a bare-only scope to the single action Devin
|
|
46184
|
+
* can hold for it, using deny > ask > allow precedence. A map without a
|
|
46185
|
+
* catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
|
|
46186
|
+
* candidates and a narrow allowlist can never widen into a blanket allow.
|
|
46187
|
+
* Returns `undefined` for an empty map: like any other empty category it
|
|
46188
|
+
* emits nothing, which leaves Devin's own auto-approve default in place.
|
|
46189
|
+
*/
|
|
46190
|
+
function collapseBareOnlyScopeRules({ category, scope, rules, logger }) {
|
|
46191
|
+
const action = collapseRulesToSingleAction({ rules });
|
|
46192
|
+
if (action === void 0) return;
|
|
46193
|
+
if (hasPatternSpecificRules(rules)) logger?.warn(`Devin accepts "${scope}" only as a bare tool name, with no pattern matcher. Collapsed the "${category}" pattern rules to "${action}" using deny > ask > allow precedence.`);
|
|
46194
|
+
return action;
|
|
46195
|
+
}
|
|
46196
|
+
/**
|
|
44016
46197
|
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
44017
46198
|
*/
|
|
44018
|
-
function convertRulesyncToDevinPermissions(config) {
|
|
46199
|
+
function convertRulesyncToDevinPermissions({ config, logger }) {
|
|
44019
46200
|
const allow = [];
|
|
44020
46201
|
const ask = [];
|
|
44021
46202
|
const deny = [];
|
|
46203
|
+
const push = (entry, action) => {
|
|
46204
|
+
switch (action) {
|
|
46205
|
+
case "allow":
|
|
46206
|
+
allow.push(entry);
|
|
46207
|
+
break;
|
|
46208
|
+
case "ask":
|
|
46209
|
+
ask.push(entry);
|
|
46210
|
+
break;
|
|
46211
|
+
case "deny": deny.push(entry);
|
|
46212
|
+
}
|
|
46213
|
+
};
|
|
44022
46214
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
44023
46215
|
const scope = toDevinScope(category);
|
|
44024
|
-
|
|
44025
|
-
const
|
|
44026
|
-
|
|
44027
|
-
|
|
44028
|
-
|
|
44029
|
-
|
|
44030
|
-
|
|
44031
|
-
|
|
44032
|
-
|
|
44033
|
-
case "deny": deny.push(entry);
|
|
44034
|
-
}
|
|
46216
|
+
if (DEVIN_BARE_ONLY_SCOPES.has(scope)) {
|
|
46217
|
+
const action = collapseBareOnlyScopeRules({
|
|
46218
|
+
category,
|
|
46219
|
+
scope,
|
|
46220
|
+
rules,
|
|
46221
|
+
logger
|
|
46222
|
+
});
|
|
46223
|
+
if (action !== void 0) push(scope, action);
|
|
46224
|
+
continue;
|
|
44035
46225
|
}
|
|
46226
|
+
for (const [pattern, action] of Object.entries(rules)) push(buildDevinPermissionEntry(scope, pattern), action);
|
|
44036
46227
|
}
|
|
44037
46228
|
return {
|
|
44038
46229
|
allow,
|
|
@@ -46278,20 +48469,14 @@ const OPENCODE_ACTION_ONLY_PERMISSION_KEYS = /* @__PURE__ */ new Set([
|
|
|
46278
48469
|
"question",
|
|
46279
48470
|
"doom_loop"
|
|
46280
48471
|
]);
|
|
46281
|
-
const PERMISSION_ACTION_PRIORITY = {
|
|
46282
|
-
allow: 0,
|
|
46283
|
-
ask: 1,
|
|
46284
|
-
deny: 2
|
|
46285
|
-
};
|
|
46286
48472
|
function toOpencodePermission({ category, value, logger }) {
|
|
46287
48473
|
if (typeof value === "string" || !OPENCODE_ACTION_ONLY_PERMISSION_KEYS.has(category)) return value;
|
|
46288
|
-
const
|
|
46289
|
-
if (
|
|
48474
|
+
const action = collapseRulesToSingleAction({ rules: value });
|
|
48475
|
+
if (action === void 0) {
|
|
46290
48476
|
logger?.warn(`OpenCode's "${category}" permission accepts only a single action. Collapsed its empty pattern map to "deny" to avoid falling back to OpenCode's default allow behavior.`);
|
|
46291
48477
|
return "deny";
|
|
46292
48478
|
}
|
|
46293
|
-
|
|
46294
|
-
if (Object.keys(value).some((pattern) => pattern !== "*")) logger?.warn(`OpenCode's "${category}" permission accepts only a single action. Collapsed its pattern rules to "${action}" using deny > ask > allow precedence.`);
|
|
48479
|
+
if (hasPatternSpecificRules(value)) logger?.warn(`OpenCode's "${category}" permission accepts only a single action. Collapsed its pattern rules to "${action}" using deny > ask > allow precedence.`);
|
|
46295
48480
|
return action;
|
|
46296
48481
|
}
|
|
46297
48482
|
/**
|
|
@@ -46669,6 +48854,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
46669
48854
|
"visible",
|
|
46670
48855
|
"eager",
|
|
46671
48856
|
"listDirectory",
|
|
48857
|
+
"todoWrite",
|
|
46672
48858
|
"workflowsEnabled"
|
|
46673
48859
|
];
|
|
46674
48860
|
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
@@ -46774,6 +48960,11 @@ const QWEN_OVERRIDE_GROUPS = [{
|
|
|
46774
48960
|
rule: "global-machine-wide",
|
|
46775
48961
|
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
|
|
46776
48962
|
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `list_directory` tool is registered for every project on this machine."
|
|
48963
|
+
},
|
|
48964
|
+
todoWrite: {
|
|
48965
|
+
rule: "global-machine-wide",
|
|
48966
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`todo_write\` tool is registered in this repository.`,
|
|
48967
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
|
|
46777
48968
|
}
|
|
46778
48969
|
}
|
|
46779
48970
|
}, {
|
|
@@ -48580,9 +50771,25 @@ function widenToPrefixGlob(pattern) {
|
|
|
48580
50771
|
function toShellEntry(prefix) {
|
|
48581
50772
|
return prefix === "" ? SHELL_TOOL_NAME : `${SHELL_TOOL_NAME}(${prefix})`;
|
|
48582
50773
|
}
|
|
48583
|
-
/**
|
|
50774
|
+
/**
|
|
50775
|
+
* The `bash` pattern a `run_shell_command(<prefix>)` entry stands for. A
|
|
50776
|
+
* prefix Tabnine matches literally, so one an author spelled glob-like
|
|
50777
|
+
* (`git *`, `*`) is read as the glob it looks like rather than doubled into
|
|
50778
|
+
* `git * *`, which no comparison or warning could make sense of.
|
|
50779
|
+
*/
|
|
48584
50780
|
function toShellPattern(prefix) {
|
|
48585
|
-
|
|
50781
|
+
if (prefix === void 0 || prefix === "" || prefix === "*") return "*";
|
|
50782
|
+
return prefix.endsWith(" *") ? prefix : `${prefix} *`;
|
|
50783
|
+
}
|
|
50784
|
+
/**
|
|
50785
|
+
* Whether {@link toShellPattern} read the prefix as a glob: such an entry
|
|
50786
|
+
* would never fire on Tabnine as spelled (the prefix is matched literally, so
|
|
50787
|
+
* `run_shell_command(git *)` only matches a command line starting with the
|
|
50788
|
+
* two characters `git *`), and rulesync writes it back as the prefix the glob
|
|
50789
|
+
* denotes — a wider entry that the author is told about.
|
|
50790
|
+
*/
|
|
50791
|
+
function isGlobSpelledPrefix(prefix) {
|
|
50792
|
+
return prefix !== void 0 && (prefix === "*" || prefix.endsWith(" *"));
|
|
48586
50793
|
}
|
|
48587
50794
|
/**
|
|
48588
50795
|
* Split a `tools.allowed`/`tools.exclude` entry into its tool name and the
|
|
@@ -48708,7 +50915,7 @@ function collectTrustAffectingOverrideEntries({ tools, general }) {
|
|
|
48708
50915
|
const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
|
|
48709
50916
|
if (allowed.length > 0) entries.push({
|
|
48710
50917
|
label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
|
|
48711
|
-
reason: "auto-approves what it names as the override spells it; a non-shell entry
|
|
50918
|
+
reason: "auto-approves what it names as the override spells it; a non-shell entry is only checked against a whole-tool deny of its tool"
|
|
48712
50919
|
});
|
|
48713
50920
|
if (Object.hasOwn(tools, SANDBOX_KEY) && isNotTrue(tools[SANDBOX_KEY])) entries.push({
|
|
48714
50921
|
label: `tools.sandbox = ${quoteValueForWarning(tools[SANDBOX_KEY])}`,
|
|
@@ -48819,6 +51026,54 @@ function buildToolLists({ permission, overrideShellAllowPatterns, logger }) {
|
|
|
48819
51026
|
};
|
|
48820
51027
|
}
|
|
48821
51028
|
/**
|
|
51029
|
+
* Names the glob-spelled `run_shell_command(...)` override entries that were
|
|
51030
|
+
* written as the prefix their glob denotes. Said only for the entries that
|
|
51031
|
+
* made it into the list: one that a deny or ask overlaps is announced as
|
|
51032
|
+
* withheld by the comparison instead, and one whose glob names no prefix at
|
|
51033
|
+
* all is skipped by it.
|
|
51034
|
+
*/
|
|
51035
|
+
function warnAboutGlobSpelledOverrideEntries({ entries, allowed, logger }) {
|
|
51036
|
+
for (const { entry, pattern } of entries) {
|
|
51037
|
+
const prefix = toShellPrefix(pattern);
|
|
51038
|
+
const written = prefix === void 0 ? void 0 : toShellEntry(prefix);
|
|
51039
|
+
if (written === void 0 || !allowed.includes(written)) continue;
|
|
51040
|
+
warnWithFallback(logger, `Tabnine CLI permissions: read tools.allowed entry ${quoteValueForWarning(entry)} of the tabnine override as the 'bash' allow rule ${quoteValueForWarning(pattern)}; Tabnine matches a prefix literally, so it is written as ${quoteValueForWarning(written)}, which auto-approves every command it covers.`);
|
|
51041
|
+
}
|
|
51042
|
+
}
|
|
51043
|
+
/**
|
|
51044
|
+
* A verbatim override allow naming a tool the canonical block denies is
|
|
51045
|
+
* withheld the way a shadowed `bash` allow is: Tabnine never loads an excluded
|
|
51046
|
+
* tool whatever `allowed` says, so the entry would only ever contradict the
|
|
51047
|
+
* deny it sits beside — and an override is not a way around a canonical rule.
|
|
51048
|
+
* Returns the entries that may be written.
|
|
51049
|
+
*/
|
|
51050
|
+
function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logger }) {
|
|
51051
|
+
const denied = overrideVerbatimAllowed.filter((entry) => {
|
|
51052
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
51053
|
+
return toolName !== void 0 && exclude.includes(toolName);
|
|
51054
|
+
});
|
|
51055
|
+
if (denied.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: withheld ${denied.length} tools.allowed entry(ies) of the tabnine override (${denied.map(quoteValueForWarning).join(", ")}) that name a tool the canonical block denies; Tabnine never loads an excluded tool, so remove the deny rule from .rulesync/permissions.jsonc to allow it.`);
|
|
51056
|
+
return overrideVerbatimAllowed.filter((entry) => !denied.includes(entry));
|
|
51057
|
+
}
|
|
51058
|
+
/**
|
|
51059
|
+
* The entries of a managed tool that the canonical block did not re-derive are
|
|
51060
|
+
* named rather than removed silently. Dropping an exclude loosens the policy,
|
|
51061
|
+
* so it is a warning; dropping an allow only brings back Tabnine's confirmation
|
|
51062
|
+
* prompt, so it is said at info level for the author who wonders where a
|
|
51063
|
+
* hand-written entry went.
|
|
51064
|
+
*/
|
|
51065
|
+
function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
|
|
51066
|
+
const dropped = (key, list) => (0, es_toolkit.uniq)(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
|
|
51067
|
+
if (list.includes(entry)) return false;
|
|
51068
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
51069
|
+
return toolName === void 0 || !list.includes(toolName);
|
|
51070
|
+
});
|
|
51071
|
+
const droppedExclude = dropped(EXCLUDE_KEY, excludeList);
|
|
51072
|
+
if (droppedExclude.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: removed ${droppedExclude.length} existing tools.exclude entry(ies) (${droppedExclude.map(quoteValueForWarning).join(", ")}) of a tool the canonical block manages; add a deny rule to .rulesync/permissions.jsonc to keep them.`);
|
|
51073
|
+
const droppedAllowed = dropped(ALLOWED_KEY, allowedList);
|
|
51074
|
+
if (droppedAllowed.length > 0) (logger ?? moduleLogger$1).info(`Tabnine CLI permissions: removed ${droppedAllowed.length} existing tools.allowed entry(ies) (${droppedAllowed.map(quoteValueForWarning).join(", ")}) of a tool the canonical block manages; add an allow rule to .rulesync/permissions.jsonc to keep them.`);
|
|
51075
|
+
}
|
|
51076
|
+
/**
|
|
48822
51077
|
* Permissions generator for the Tabnine CLI.
|
|
48823
51078
|
*
|
|
48824
51079
|
* Tabnine CLI keeps its tool policy in `.tabnine/agent/settings.json` (project)
|
|
@@ -48884,7 +51139,10 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48884
51139
|
const config = rulesyncPermissions.getJson();
|
|
48885
51140
|
const override = isRecord$1(config.tabnine) ? config.tabnine : {};
|
|
48886
51141
|
const { tools: overrideTools, general: overrideGeneral, refused: refusedPaths } = stripRefusedOverridePaths(override);
|
|
48887
|
-
if (refusedPaths.length > 0)
|
|
51142
|
+
if (refusedPaths.length > 0) {
|
|
51143
|
+
const one = refusedPaths.length === 1;
|
|
51144
|
+
warnWithFallback(logger, `Tabnine CLI permissions: refused to write ${refusedPaths.join(", ")} from the tabnine override; Tabnine CLI runs ${one ? "that value" : "those values"} as a command or sends its traffic there, and a permissions file (one that came from 'rulesync fetch' included) must not be able to point it at an executable or a server of its choosing. Set ${one ? "it" : "them"} by hand in ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} if you need ${one ? "it" : "them"}.`);
|
|
51145
|
+
}
|
|
48888
51146
|
const overrideList = (key) => {
|
|
48889
51147
|
const value = overrideTools[key];
|
|
48890
51148
|
if (value === void 0 || isStringArray$2(value)) return value ?? [];
|
|
@@ -48893,10 +51151,17 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48893
51151
|
};
|
|
48894
51152
|
const overrideShellAllowPatterns = [];
|
|
48895
51153
|
const overrideVerbatimAllowed = [];
|
|
51154
|
+
const globSpelledOverrideEntries = [];
|
|
48896
51155
|
for (const entry of overrideList(ALLOWED_KEY)) {
|
|
48897
51156
|
const parsed = parseTabnineEntry(entry);
|
|
48898
|
-
if (parsed?.toolName === SHELL_TOOL_NAME)
|
|
48899
|
-
|
|
51157
|
+
if (parsed?.toolName === SHELL_TOOL_NAME) {
|
|
51158
|
+
const pattern = toShellPattern(parsed.prefix);
|
|
51159
|
+
if (isGlobSpelledPrefix(parsed.prefix)) globSpelledOverrideEntries.push({
|
|
51160
|
+
entry,
|
|
51161
|
+
pattern
|
|
51162
|
+
});
|
|
51163
|
+
overrideShellAllowPatterns.push(pattern);
|
|
51164
|
+
} else overrideVerbatimAllowed.push(entry);
|
|
48900
51165
|
}
|
|
48901
51166
|
const overrideExclude = overrideList(EXCLUDE_KEY);
|
|
48902
51167
|
const { allowed, exclude } = buildToolLists({
|
|
@@ -48904,6 +51169,16 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48904
51169
|
overrideShellAllowPatterns,
|
|
48905
51170
|
logger
|
|
48906
51171
|
});
|
|
51172
|
+
warnAboutGlobSpelledOverrideEntries({
|
|
51173
|
+
entries: globSpelledOverrideEntries,
|
|
51174
|
+
allowed,
|
|
51175
|
+
logger
|
|
51176
|
+
});
|
|
51177
|
+
const writableOverrideAllowed = withholdDeniedOverrideAllowed({
|
|
51178
|
+
overrideVerbatimAllowed,
|
|
51179
|
+
exclude,
|
|
51180
|
+
logger
|
|
51181
|
+
});
|
|
48907
51182
|
const ignoredOverrideKeys = Object.keys(override).filter((key) => key !== TOOLS_KEY && key !== GENERAL_KEY);
|
|
48908
51183
|
if (ignoredOverrideKeys.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: ignored ${ignoredOverrideKeys.length} key(s) of the tabnine override (${ignoredOverrideKeys.map(quoteValueForWarning).join(", ")}); only 'tools' and 'general' are written through the permissions feature.`);
|
|
48909
51184
|
const existingTools = this.existingToolsGroup(existingContent);
|
|
@@ -48914,7 +51189,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48914
51189
|
});
|
|
48915
51190
|
const allowedList = (0, es_toolkit.uniq)([
|
|
48916
51191
|
...allowed,
|
|
48917
|
-
...
|
|
51192
|
+
...writableOverrideAllowed,
|
|
48918
51193
|
...preservedEntries(ALLOWED_KEY)
|
|
48919
51194
|
]);
|
|
48920
51195
|
const excludeList = (0, es_toolkit.uniq)([
|
|
@@ -48922,12 +51197,12 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48922
51197
|
...overrideExclude,
|
|
48923
51198
|
...preservedEntries(EXCLUDE_KEY)
|
|
48924
51199
|
]);
|
|
48925
|
-
|
|
48926
|
-
|
|
48927
|
-
|
|
48928
|
-
|
|
51200
|
+
reportDroppedManagedEntries({
|
|
51201
|
+
existingTools,
|
|
51202
|
+
allowedList,
|
|
51203
|
+
excludeList,
|
|
51204
|
+
logger
|
|
48929
51205
|
});
|
|
48930
|
-
if (droppedExcludeEntries.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: removed ${droppedExcludeEntries.length} existing tools.exclude entry(ies) (${droppedExcludeEntries.map(quoteValueForWarning).join(", ")}) of a tool the canonical block manages; add a deny rule to .rulesync/permissions.jsonc to keep them.`);
|
|
48931
51206
|
const tools = {
|
|
48932
51207
|
...overrideTools,
|
|
48933
51208
|
[ALLOWED_KEY]: allowedList.length > 0 ? allowedList : void 0,
|
|
@@ -48941,7 +51216,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48941
51216
|
entries: collectTrustAffectingOverrideEntries({
|
|
48942
51217
|
tools: {
|
|
48943
51218
|
...overrideTools,
|
|
48944
|
-
[ALLOWED_KEY]:
|
|
51219
|
+
[ALLOWED_KEY]: writableOverrideAllowed
|
|
48945
51220
|
},
|
|
48946
51221
|
general: overrideGeneral
|
|
48947
51222
|
}),
|
|
@@ -52274,6 +54549,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52274
54549
|
supportsImport: true
|
|
52275
54550
|
}
|
|
52276
54551
|
}],
|
|
54552
|
+
["commandcode", {
|
|
54553
|
+
class: CommandcodePermissions,
|
|
54554
|
+
meta: {
|
|
54555
|
+
supportsProject: true,
|
|
54556
|
+
supportsGlobal: true,
|
|
54557
|
+
supportsImport: true
|
|
54558
|
+
}
|
|
54559
|
+
}],
|
|
52277
54560
|
["continue", {
|
|
52278
54561
|
class: ContinuePermissions,
|
|
52279
54562
|
meta: {
|
|
@@ -52298,6 +54581,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52298
54581
|
supportsImport: true
|
|
52299
54582
|
}
|
|
52300
54583
|
}],
|
|
54584
|
+
["crush", {
|
|
54585
|
+
class: CrushPermissions,
|
|
54586
|
+
meta: {
|
|
54587
|
+
supportsProject: true,
|
|
54588
|
+
supportsGlobal: true,
|
|
54589
|
+
supportsImport: true
|
|
54590
|
+
}
|
|
54591
|
+
}],
|
|
52301
54592
|
["cursor", {
|
|
52302
54593
|
class: CursorPermissions,
|
|
52303
54594
|
meta: {
|
|
@@ -55032,6 +57323,154 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
55032
57323
|
}
|
|
55033
57324
|
};
|
|
55034
57325
|
//#endregion
|
|
57326
|
+
//#region src/features/skills/commandcode-skill.ts
|
|
57327
|
+
const CommandcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
57328
|
+
name: zod_mini.z.string(),
|
|
57329
|
+
description: zod_mini.z.string()
|
|
57330
|
+
});
|
|
57331
|
+
/**
|
|
57332
|
+
* Represents a Command Code skill directory.
|
|
57333
|
+
*
|
|
57334
|
+
* Command Code discovers Anthropic-style `<name>/SKILL.md` directories under
|
|
57335
|
+
* `<project>/.commandcode/skills/` (project scope) and
|
|
57336
|
+
* `~/.commandcode/skills/` (user scope). The frontmatter requires `name`
|
|
57337
|
+
* (matching the directory name) and `description`; supporting files next to
|
|
57338
|
+
* `SKILL.md` are carried along.
|
|
57339
|
+
*
|
|
57340
|
+
* @see https://commandcode.ai/docs/skills
|
|
57341
|
+
*/
|
|
57342
|
+
var CommandcodeSkill = class CommandcodeSkill extends ToolSkill {
|
|
57343
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = COMMANDCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
57344
|
+
super({
|
|
57345
|
+
outputRoot,
|
|
57346
|
+
relativeDirPath,
|
|
57347
|
+
dirName,
|
|
57348
|
+
mainFile: {
|
|
57349
|
+
name: SKILL_FILE_NAME,
|
|
57350
|
+
body,
|
|
57351
|
+
frontmatter: { ...frontmatter }
|
|
57352
|
+
},
|
|
57353
|
+
otherFiles,
|
|
57354
|
+
global
|
|
57355
|
+
});
|
|
57356
|
+
if (validate) {
|
|
57357
|
+
const result = this.validate();
|
|
57358
|
+
if (!result.success) throw result.error;
|
|
57359
|
+
}
|
|
57360
|
+
}
|
|
57361
|
+
static getSettablePaths(_options = {}) {
|
|
57362
|
+
return { relativeDirPath: COMMANDCODE_SKILLS_DIR_PATH };
|
|
57363
|
+
}
|
|
57364
|
+
getFrontmatter() {
|
|
57365
|
+
return CommandcodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
57366
|
+
}
|
|
57367
|
+
getBody() {
|
|
57368
|
+
return this.mainFile?.body ?? "";
|
|
57369
|
+
}
|
|
57370
|
+
validate() {
|
|
57371
|
+
if (!this.mainFile) return {
|
|
57372
|
+
success: false,
|
|
57373
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
57374
|
+
};
|
|
57375
|
+
const result = CommandcodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
57376
|
+
if (!result.success) return {
|
|
57377
|
+
success: false,
|
|
57378
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
57379
|
+
};
|
|
57380
|
+
if (result.data.name !== this.getDirName()) return {
|
|
57381
|
+
success: false,
|
|
57382
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: frontmatter name (${result.data.name}) must match directory name (${this.getDirName()})`)
|
|
57383
|
+
};
|
|
57384
|
+
return {
|
|
57385
|
+
success: true,
|
|
57386
|
+
error: null
|
|
57387
|
+
};
|
|
57388
|
+
}
|
|
57389
|
+
toRulesyncSkill() {
|
|
57390
|
+
const { name, description, ...commandcodeSection } = this.getFrontmatter();
|
|
57391
|
+
const rulesyncFrontmatter = {
|
|
57392
|
+
name,
|
|
57393
|
+
description,
|
|
57394
|
+
targets: ["*"],
|
|
57395
|
+
...Object.keys(commandcodeSection).length > 0 && { commandcode: commandcodeSection }
|
|
57396
|
+
};
|
|
57397
|
+
return new RulesyncSkill({
|
|
57398
|
+
outputRoot: this.outputRoot,
|
|
57399
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
57400
|
+
dirName: this.getDirName(),
|
|
57401
|
+
frontmatter: rulesyncFrontmatter,
|
|
57402
|
+
body: this.getBody(),
|
|
57403
|
+
otherFiles: this.getOtherFiles(),
|
|
57404
|
+
validate: true,
|
|
57405
|
+
global: this.global
|
|
57406
|
+
});
|
|
57407
|
+
}
|
|
57408
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
57409
|
+
const settablePaths = CommandcodeSkill.getSettablePaths({ global });
|
|
57410
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
57411
|
+
const { name: _sectionName, description: _sectionDescription, ...commandcodeSection } = rulesyncFrontmatter.commandcode ?? {};
|
|
57412
|
+
const commandcodeFrontmatter = {
|
|
57413
|
+
...commandcodeSection,
|
|
57414
|
+
name: rulesyncFrontmatter.name,
|
|
57415
|
+
description: rulesyncFrontmatter.description
|
|
57416
|
+
};
|
|
57417
|
+
return new CommandcodeSkill({
|
|
57418
|
+
outputRoot,
|
|
57419
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
57420
|
+
dirName: commandcodeFrontmatter.name,
|
|
57421
|
+
frontmatter: commandcodeFrontmatter,
|
|
57422
|
+
body: rulesyncSkill.getBody(),
|
|
57423
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
57424
|
+
validate,
|
|
57425
|
+
global
|
|
57426
|
+
});
|
|
57427
|
+
}
|
|
57428
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
57429
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
57430
|
+
return targets.includes("*") || targets.includes("commandcode");
|
|
57431
|
+
}
|
|
57432
|
+
static async fromDir(params) {
|
|
57433
|
+
const loaded = await this.loadSkillDirContent({
|
|
57434
|
+
...params,
|
|
57435
|
+
getSettablePaths: CommandcodeSkill.getSettablePaths
|
|
57436
|
+
});
|
|
57437
|
+
const result = CommandcodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
57438
|
+
if (!result.success) {
|
|
57439
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
57440
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
57441
|
+
}
|
|
57442
|
+
if (result.data.name !== loaded.dirName) {
|
|
57443
|
+
const skillFilePath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName, SKILL_FILE_NAME);
|
|
57444
|
+
throw new Error(`Frontmatter name (${result.data.name}) must match directory name (${loaded.dirName}) in ${skillFilePath}`);
|
|
57445
|
+
}
|
|
57446
|
+
return new CommandcodeSkill({
|
|
57447
|
+
outputRoot: loaded.outputRoot,
|
|
57448
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
57449
|
+
dirName: loaded.dirName,
|
|
57450
|
+
frontmatter: result.data,
|
|
57451
|
+
body: loaded.body,
|
|
57452
|
+
otherFiles: loaded.otherFiles,
|
|
57453
|
+
validate: true,
|
|
57454
|
+
global: loaded.global
|
|
57455
|
+
});
|
|
57456
|
+
}
|
|
57457
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
57458
|
+
return new CommandcodeSkill({
|
|
57459
|
+
outputRoot,
|
|
57460
|
+
relativeDirPath,
|
|
57461
|
+
dirName,
|
|
57462
|
+
frontmatter: {
|
|
57463
|
+
name: "",
|
|
57464
|
+
description: ""
|
|
57465
|
+
},
|
|
57466
|
+
body: "",
|
|
57467
|
+
otherFiles: [],
|
|
57468
|
+
validate: false,
|
|
57469
|
+
global
|
|
57470
|
+
});
|
|
57471
|
+
}
|
|
57472
|
+
};
|
|
57473
|
+
//#endregion
|
|
55035
57474
|
//#region src/features/skills/continue-skill.ts
|
|
55036
57475
|
const ContinueSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
55037
57476
|
name: zod_mini.z.string(),
|
|
@@ -58319,6 +60758,183 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
58319
60758
|
}
|
|
58320
60759
|
};
|
|
58321
60760
|
//#endregion
|
|
60761
|
+
//#region src/constants/pool-paths.ts
|
|
60762
|
+
/**
|
|
60763
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
60764
|
+
*
|
|
60765
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
60766
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
60767
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
60768
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
60769
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
60770
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
60771
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
60772
|
+
*
|
|
60773
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
60774
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
60775
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
60776
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
60777
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
60778
|
+
*
|
|
60779
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
60780
|
+
* @see https://docs.poolside.ai/skills
|
|
60781
|
+
* @see https://github.com/poolsideai/pool
|
|
60782
|
+
*/
|
|
60783
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
60784
|
+
const POOL_DIR = ".poolside";
|
|
60785
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
60786
|
+
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
60787
|
+
/** Project skills root, relative to the project root. */
|
|
60788
|
+
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
60789
|
+
/** Global skills root, relative to the home directory. */
|
|
60790
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
60791
|
+
//#endregion
|
|
60792
|
+
//#region src/features/skills/pool-skill.ts
|
|
60793
|
+
const PoolSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
60794
|
+
name: zod_mini.z.string(),
|
|
60795
|
+
description: zod_mini.z.string()
|
|
60796
|
+
});
|
|
60797
|
+
/**
|
|
60798
|
+
* Represents a Pool (Poolside) skill directory.
|
|
60799
|
+
*
|
|
60800
|
+
* Pool discovers Agent Skills (`<name>/SKILL.md` bundles, optionally with
|
|
60801
|
+
* supporting files) from `.poolside/skills/` (project) and
|
|
60802
|
+
* `~/.config/poolside/skills/` (global). It also scans the shared
|
|
60803
|
+
* `.agents/skills/` / `~/.agents/skills/` roots and the skill directories of
|
|
60804
|
+
* other Agent Skills tools; those belong to their own targets, so this class
|
|
60805
|
+
* writes only the two Pool-specific roots and a skill is written exactly once.
|
|
60806
|
+
* Pool requires the directory name to equal the frontmatter `name`, otherwise
|
|
60807
|
+
* it skips the skill. Unlike adapters that reject such a skill outright, this
|
|
60808
|
+
* one warns and still writes it: the directory name is the canonical identity
|
|
60809
|
+
* shared with every other target, and Pool merely skips the skill rather than
|
|
60810
|
+
* failing, so a hard error here would block the other targets for a
|
|
60811
|
+
* Pool-only concern. Import is lenient for the same reason.
|
|
60812
|
+
* @see https://docs.poolside.ai/skills
|
|
60813
|
+
*/
|
|
60814
|
+
var PoolSkill = class PoolSkill extends ToolSkill {
|
|
60815
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = POOL_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
60816
|
+
super({
|
|
60817
|
+
outputRoot,
|
|
60818
|
+
relativeDirPath,
|
|
60819
|
+
dirName,
|
|
60820
|
+
mainFile: {
|
|
60821
|
+
name: SKILL_FILE_NAME,
|
|
60822
|
+
body,
|
|
60823
|
+
frontmatter: { ...frontmatter }
|
|
60824
|
+
},
|
|
60825
|
+
otherFiles,
|
|
60826
|
+
global
|
|
60827
|
+
});
|
|
60828
|
+
if (validate) {
|
|
60829
|
+
const result = this.validate();
|
|
60830
|
+
if (!result.success) throw result.error;
|
|
60831
|
+
}
|
|
60832
|
+
}
|
|
60833
|
+
static getSettablePaths({ global = false } = {}) {
|
|
60834
|
+
return { relativeDirPath: global ? POOL_GLOBAL_SKILLS_DIR_PATH : POOL_SKILLS_DIR_PATH };
|
|
60835
|
+
}
|
|
60836
|
+
getFrontmatter() {
|
|
60837
|
+
return PoolSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
60838
|
+
}
|
|
60839
|
+
getBody() {
|
|
60840
|
+
return this.mainFile?.body ?? "";
|
|
60841
|
+
}
|
|
60842
|
+
validate() {
|
|
60843
|
+
if (!this.mainFile) return {
|
|
60844
|
+
success: false,
|
|
60845
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
60846
|
+
};
|
|
60847
|
+
const result = PoolSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
60848
|
+
if (!result.success) return {
|
|
60849
|
+
success: false,
|
|
60850
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
60851
|
+
};
|
|
60852
|
+
return {
|
|
60853
|
+
success: true,
|
|
60854
|
+
error: null
|
|
60855
|
+
};
|
|
60856
|
+
}
|
|
60857
|
+
toRulesyncSkill() {
|
|
60858
|
+
const frontmatter = this.getFrontmatter();
|
|
60859
|
+
const rulesyncFrontmatter = {
|
|
60860
|
+
name: frontmatter.name,
|
|
60861
|
+
description: frontmatter.description,
|
|
60862
|
+
targets: ["*"]
|
|
60863
|
+
};
|
|
60864
|
+
return new RulesyncSkill({
|
|
60865
|
+
outputRoot: this.outputRoot,
|
|
60866
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
60867
|
+
dirName: this.getDirName(),
|
|
60868
|
+
frontmatter: rulesyncFrontmatter,
|
|
60869
|
+
body: this.getBody(),
|
|
60870
|
+
otherFiles: this.getOtherFiles(),
|
|
60871
|
+
validate: true,
|
|
60872
|
+
global: this.global
|
|
60873
|
+
});
|
|
60874
|
+
}
|
|
60875
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
60876
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
60877
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
60878
|
+
const dirName = rulesyncSkill.getDirName();
|
|
60879
|
+
const poolFrontmatter = {
|
|
60880
|
+
name: rulesyncFrontmatter.name,
|
|
60881
|
+
description: rulesyncFrontmatter.description
|
|
60882
|
+
};
|
|
60883
|
+
if (poolFrontmatter.name !== dirName) warnWithFallback(logger, `${stripControlCharacters(toPosixPath((0, node_path.join)(outputRoot, settablePaths.relativeDirPath, dirName, SKILL_FILE_NAME)))}: \`name\` ${quoteForLog(poolFrontmatter.name)} does not match its directory name ${quoteForLog(dirName)}; Pool only loads a skill whose directory name equals its \`name\``);
|
|
60884
|
+
return new PoolSkill({
|
|
60885
|
+
outputRoot,
|
|
60886
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
60887
|
+
dirName,
|
|
60888
|
+
frontmatter: poolFrontmatter,
|
|
60889
|
+
body: rulesyncSkill.getBody(),
|
|
60890
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
60891
|
+
validate,
|
|
60892
|
+
global
|
|
60893
|
+
});
|
|
60894
|
+
}
|
|
60895
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
60896
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
60897
|
+
return targets.includes("*") || targets.includes("pool");
|
|
60898
|
+
}
|
|
60899
|
+
static async fromDir(params) {
|
|
60900
|
+
const loaded = await this.loadSkillDirContent({
|
|
60901
|
+
...params,
|
|
60902
|
+
getSettablePaths: PoolSkill.getSettablePaths
|
|
60903
|
+
});
|
|
60904
|
+
const result = PoolSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
60905
|
+
if (!result.success) {
|
|
60906
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
60907
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
60908
|
+
}
|
|
60909
|
+
return new PoolSkill({
|
|
60910
|
+
outputRoot: loaded.outputRoot,
|
|
60911
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
60912
|
+
dirName: loaded.dirName,
|
|
60913
|
+
frontmatter: result.data,
|
|
60914
|
+
body: loaded.body,
|
|
60915
|
+
otherFiles: loaded.otherFiles,
|
|
60916
|
+
validate: true,
|
|
60917
|
+
global: loaded.global
|
|
60918
|
+
});
|
|
60919
|
+
}
|
|
60920
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
60921
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
60922
|
+
return new PoolSkill({
|
|
60923
|
+
outputRoot,
|
|
60924
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
60925
|
+
dirName,
|
|
60926
|
+
frontmatter: {
|
|
60927
|
+
name: "",
|
|
60928
|
+
description: ""
|
|
60929
|
+
},
|
|
60930
|
+
body: "",
|
|
60931
|
+
otherFiles: [],
|
|
60932
|
+
validate: false,
|
|
60933
|
+
global
|
|
60934
|
+
});
|
|
60935
|
+
}
|
|
60936
|
+
};
|
|
60937
|
+
//#endregion
|
|
58322
60938
|
//#region src/features/skills/qwencode-skill.ts
|
|
58323
60939
|
const QwencodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
58324
60940
|
name: zod_mini.z.string(),
|
|
@@ -60044,6 +62660,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60044
62660
|
supportsGlobal: true
|
|
60045
62661
|
}
|
|
60046
62662
|
}],
|
|
62663
|
+
["commandcode", {
|
|
62664
|
+
class: CommandcodeSkill,
|
|
62665
|
+
meta: {
|
|
62666
|
+
supportsProject: true,
|
|
62667
|
+
supportsSimulated: false,
|
|
62668
|
+
supportsGlobal: true
|
|
62669
|
+
}
|
|
62670
|
+
}],
|
|
60047
62671
|
["continue", {
|
|
60048
62672
|
class: ContinueSkill,
|
|
60049
62673
|
meta: {
|
|
@@ -60205,6 +62829,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60205
62829
|
supportsGlobal: true
|
|
60206
62830
|
}
|
|
60207
62831
|
}],
|
|
62832
|
+
["pool", {
|
|
62833
|
+
class: PoolSkill,
|
|
62834
|
+
meta: {
|
|
62835
|
+
supportsProject: true,
|
|
62836
|
+
supportsSimulated: false,
|
|
62837
|
+
supportsGlobal: true
|
|
62838
|
+
}
|
|
62839
|
+
}],
|
|
60208
62840
|
["qwencode", {
|
|
60209
62841
|
class: QwencodeSkill,
|
|
60210
62842
|
meta: {
|
|
@@ -62139,6 +64771,143 @@ var CodexCliSubagent = class CodexCliSubagent extends ToolSubagent {
|
|
|
62139
64771
|
}
|
|
62140
64772
|
};
|
|
62141
64773
|
//#endregion
|
|
64774
|
+
//#region src/features/subagents/commandcode-subagent.ts
|
|
64775
|
+
const CommandcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
64776
|
+
name: zod_mini.z.string(),
|
|
64777
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
64778
|
+
tools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
64779
|
+
disallowedTools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
64780
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
64781
|
+
reasoningEffort: zod_mini.z.optional(zod_mini.z.string()),
|
|
64782
|
+
maxTurns: zod_mini.z.optional(zod_mini.z.number()),
|
|
64783
|
+
permissionMode: zod_mini.z.optional(zod_mini.z.string()),
|
|
64784
|
+
background: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
64785
|
+
showOutput: zod_mini.z.optional(zod_mini.z.boolean())
|
|
64786
|
+
});
|
|
64787
|
+
/**
|
|
64788
|
+
* Command Code custom agent: a Markdown file with YAML frontmatter under
|
|
64789
|
+
* `.commandcode/agents/` (project) or `~/.commandcode/agents/` (user); the
|
|
64790
|
+
* body is the agent's system prompt.
|
|
64791
|
+
*
|
|
64792
|
+
* @see https://commandcode.ai/docs/custom-agents
|
|
64793
|
+
*/
|
|
64794
|
+
var CommandcodeSubagent = class CommandcodeSubagent extends ToolSubagent {
|
|
64795
|
+
frontmatter;
|
|
64796
|
+
body;
|
|
64797
|
+
constructor({ frontmatter, body, fileContent, ...rest }) {
|
|
64798
|
+
if (rest.validate !== false) {
|
|
64799
|
+
const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
64800
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
64801
|
+
}
|
|
64802
|
+
super({
|
|
64803
|
+
...rest,
|
|
64804
|
+
fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
|
|
64805
|
+
});
|
|
64806
|
+
this.frontmatter = frontmatter;
|
|
64807
|
+
this.body = body;
|
|
64808
|
+
}
|
|
64809
|
+
static getSettablePaths(_options = {}) {
|
|
64810
|
+
return { relativeDirPath: COMMANDCODE_AGENTS_DIR_PATH };
|
|
64811
|
+
}
|
|
64812
|
+
getFrontmatter() {
|
|
64813
|
+
return this.frontmatter;
|
|
64814
|
+
}
|
|
64815
|
+
getBody() {
|
|
64816
|
+
return this.body;
|
|
64817
|
+
}
|
|
64818
|
+
toRulesyncSubagent() {
|
|
64819
|
+
const { name, description, ...rest } = this.frontmatter;
|
|
64820
|
+
return new RulesyncSubagent({
|
|
64821
|
+
outputRoot: ".",
|
|
64822
|
+
frontmatter: {
|
|
64823
|
+
targets: ["*"],
|
|
64824
|
+
name,
|
|
64825
|
+
description,
|
|
64826
|
+
commandcode: { ...rest }
|
|
64827
|
+
},
|
|
64828
|
+
body: this.body,
|
|
64829
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
64830
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
64831
|
+
validate: true
|
|
64832
|
+
});
|
|
64833
|
+
}
|
|
64834
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
64835
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
64836
|
+
const commandcodeSection = rulesyncFrontmatter.commandcode ?? {};
|
|
64837
|
+
const commandcodeSubagentFrontmatter = {
|
|
64838
|
+
name: rulesyncFrontmatter.name,
|
|
64839
|
+
description: rulesyncFrontmatter.description,
|
|
64840
|
+
...commandcodeSection
|
|
64841
|
+
};
|
|
64842
|
+
const body = rulesyncSubagent.getBody();
|
|
64843
|
+
const fileContent = stringifyFrontmatter(body, commandcodeSubagentFrontmatter, { avoidBlockScalars: true });
|
|
64844
|
+
const paths = this.getSettablePaths({ global });
|
|
64845
|
+
return new CommandcodeSubagent({
|
|
64846
|
+
outputRoot,
|
|
64847
|
+
frontmatter: commandcodeSubagentFrontmatter,
|
|
64848
|
+
body,
|
|
64849
|
+
relativeDirPath: paths.relativeDirPath,
|
|
64850
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
64851
|
+
fileContent,
|
|
64852
|
+
validate,
|
|
64853
|
+
global
|
|
64854
|
+
});
|
|
64855
|
+
}
|
|
64856
|
+
validate() {
|
|
64857
|
+
if (!this.frontmatter) return {
|
|
64858
|
+
success: true,
|
|
64859
|
+
error: null
|
|
64860
|
+
};
|
|
64861
|
+
const result = CommandcodeSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
64862
|
+
if (result.success) return {
|
|
64863
|
+
success: true,
|
|
64864
|
+
error: null
|
|
64865
|
+
};
|
|
64866
|
+
else return {
|
|
64867
|
+
success: false,
|
|
64868
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
64869
|
+
};
|
|
64870
|
+
}
|
|
64871
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
64872
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
64873
|
+
rulesyncSubagent,
|
|
64874
|
+
toolTarget: "commandcode"
|
|
64875
|
+
});
|
|
64876
|
+
}
|
|
64877
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
64878
|
+
const paths = this.getSettablePaths({ global });
|
|
64879
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
64880
|
+
const fileContent = await readFileContent(filePath);
|
|
64881
|
+
const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
|
|
64882
|
+
const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
64883
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
64884
|
+
return new CommandcodeSubagent({
|
|
64885
|
+
outputRoot,
|
|
64886
|
+
relativeDirPath: paths.relativeDirPath,
|
|
64887
|
+
relativeFilePath,
|
|
64888
|
+
frontmatter: result.data,
|
|
64889
|
+
body: content.trim(),
|
|
64890
|
+
fileContent,
|
|
64891
|
+
validate,
|
|
64892
|
+
global
|
|
64893
|
+
});
|
|
64894
|
+
}
|
|
64895
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
64896
|
+
return new CommandcodeSubagent({
|
|
64897
|
+
outputRoot,
|
|
64898
|
+
relativeDirPath,
|
|
64899
|
+
relativeFilePath,
|
|
64900
|
+
frontmatter: {
|
|
64901
|
+
name: "",
|
|
64902
|
+
description: ""
|
|
64903
|
+
},
|
|
64904
|
+
body: "",
|
|
64905
|
+
fileContent: "",
|
|
64906
|
+
validate: false
|
|
64907
|
+
});
|
|
64908
|
+
}
|
|
64909
|
+
};
|
|
64910
|
+
//#endregion
|
|
62142
64911
|
//#region src/features/subagents/copilot-subagent.ts
|
|
62143
64912
|
const CopilotSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
62144
64913
|
name: zod_mini.z.string(),
|
|
@@ -65039,7 +67808,7 @@ var RooSubagent = class extends ToolSubagent {
|
|
|
65039
67808
|
//#region src/features/subagents/tabnine-subagent.ts
|
|
65040
67809
|
const TabnineSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
65041
67810
|
name: zod_mini.z.string(),
|
|
65042
|
-
description: zod_mini.z.
|
|
67811
|
+
description: zod_mini.z.string(),
|
|
65043
67812
|
/** "local" (default) runs inside the CLI; "remote" delegates to a Tabnine cloud agent. */
|
|
65044
67813
|
kind: zod_mini.z.optional(zod_mini.z.string()),
|
|
65045
67814
|
/** Built-in tool names the subagent may use; omitted = every tool. */
|
|
@@ -65098,14 +67867,23 @@ var TabnineSubagent = class TabnineSubagent extends ToolSubagent {
|
|
|
65098
67867
|
validate: true
|
|
65099
67868
|
});
|
|
65100
67869
|
}
|
|
65101
|
-
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
67870
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
65102
67871
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
65103
67872
|
const tabnineSection = rulesyncFrontmatter.tabnine ?? {};
|
|
65104
|
-
const
|
|
67873
|
+
const merged = {
|
|
65105
67874
|
name: rulesyncFrontmatter.name,
|
|
65106
67875
|
description: rulesyncFrontmatter.description,
|
|
65107
67876
|
...tabnineSection
|
|
65108
67877
|
};
|
|
67878
|
+
let description = merged.description;
|
|
67879
|
+
if (description === void 0 || description === "") {
|
|
67880
|
+
description = rulesyncFrontmatter.name ? `${rulesyncFrontmatter.name} subagent` : "subagent";
|
|
67881
|
+
logger?.warn(`Tabnine CLI subagent ${rulesyncSubagent.getRelativeFilePath()} has no description, which Tabnine requires; wrote ${JSON.stringify(description)} as a placeholder.`);
|
|
67882
|
+
}
|
|
67883
|
+
const tabnineSubagentFrontmatter = {
|
|
67884
|
+
...merged,
|
|
67885
|
+
description
|
|
67886
|
+
};
|
|
65109
67887
|
const body = rulesyncSubagent.getBody();
|
|
65110
67888
|
const fileContent = stringifyFrontmatter(body, tabnineSubagentFrontmatter, { avoidBlockScalars: true });
|
|
65111
67889
|
const paths = this.getSettablePaths({ global });
|
|
@@ -65788,6 +68566,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
65788
68566
|
filePattern: "*.toml"
|
|
65789
68567
|
}
|
|
65790
68568
|
}],
|
|
68569
|
+
["commandcode", {
|
|
68570
|
+
class: CommandcodeSubagent,
|
|
68571
|
+
meta: {
|
|
68572
|
+
supportsProject: true,
|
|
68573
|
+
supportsSimulated: false,
|
|
68574
|
+
supportsGlobal: true,
|
|
68575
|
+
filePattern: "*.md"
|
|
68576
|
+
}
|
|
68577
|
+
}],
|
|
65791
68578
|
["copilot", {
|
|
65792
68579
|
class: CopilotSubagent,
|
|
65793
68580
|
meta: {
|
|
@@ -68620,6 +71407,73 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
|
68620
71407
|
}
|
|
68621
71408
|
};
|
|
68622
71409
|
//#endregion
|
|
71410
|
+
//#region src/features/rules/commandcode-rule.ts
|
|
71411
|
+
var CommandcodeRule = class CommandcodeRule extends ToolRule {
|
|
71412
|
+
constructor({ fileContent, root, ...rest }) {
|
|
71413
|
+
super({
|
|
71414
|
+
...rest,
|
|
71415
|
+
fileContent,
|
|
71416
|
+
root: root ?? false
|
|
71417
|
+
});
|
|
71418
|
+
}
|
|
71419
|
+
static getSettablePaths({ global = false } = {}) {
|
|
71420
|
+
return { root: {
|
|
71421
|
+
relativeDirPath: global ? COMMANDCODE_DIR : ".",
|
|
71422
|
+
relativeFilePath: COMMANDCODE_RULE_FILE_NAME
|
|
71423
|
+
} };
|
|
71424
|
+
}
|
|
71425
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
71426
|
+
const { root } = this.getSettablePaths({ global });
|
|
71427
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
71428
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
71429
|
+
return new CommandcodeRule({
|
|
71430
|
+
outputRoot,
|
|
71431
|
+
relativeDirPath: root.relativeDirPath,
|
|
71432
|
+
relativeFilePath: root.relativeFilePath,
|
|
71433
|
+
fileContent,
|
|
71434
|
+
validate,
|
|
71435
|
+
root: true
|
|
71436
|
+
});
|
|
71437
|
+
}
|
|
71438
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
71439
|
+
const { root } = this.getSettablePaths({ global });
|
|
71440
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
71441
|
+
return new CommandcodeRule({
|
|
71442
|
+
outputRoot,
|
|
71443
|
+
relativeDirPath: root.relativeDirPath,
|
|
71444
|
+
relativeFilePath: root.relativeFilePath,
|
|
71445
|
+
fileContent: rulesyncRule.getBody(),
|
|
71446
|
+
validate,
|
|
71447
|
+
root: isRoot
|
|
71448
|
+
});
|
|
71449
|
+
}
|
|
71450
|
+
toRulesyncRule() {
|
|
71451
|
+
return this.toRulesyncRuleDefault();
|
|
71452
|
+
}
|
|
71453
|
+
validate() {
|
|
71454
|
+
return {
|
|
71455
|
+
success: true,
|
|
71456
|
+
error: null
|
|
71457
|
+
};
|
|
71458
|
+
}
|
|
71459
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
71460
|
+
return new CommandcodeRule({
|
|
71461
|
+
outputRoot,
|
|
71462
|
+
relativeDirPath,
|
|
71463
|
+
relativeFilePath,
|
|
71464
|
+
fileContent: "",
|
|
71465
|
+
validate: false,
|
|
71466
|
+
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".commandcode")
|
|
71467
|
+
});
|
|
71468
|
+
}
|
|
71469
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
71470
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
71471
|
+
rulesyncRule,
|
|
71472
|
+
toolTarget: "commandcode"
|
|
71473
|
+
});
|
|
71474
|
+
}
|
|
71475
|
+
};
|
|
71476
|
+
//#endregion
|
|
68623
71477
|
//#region src/features/rules/continue-rule.ts
|
|
68624
71478
|
/**
|
|
68625
71479
|
* Frontmatter schema for Continue rule files (`.continue/rules/*.md`).
|
|
@@ -71368,24 +74222,6 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
71368
74222
|
}
|
|
71369
74223
|
};
|
|
71370
74224
|
//#endregion
|
|
71371
|
-
//#region src/constants/pool-paths.ts
|
|
71372
|
-
/**
|
|
71373
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
71374
|
-
*
|
|
71375
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
71376
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
71377
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
71378
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
71379
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
71380
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
71381
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
71382
|
-
*
|
|
71383
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
71384
|
-
* @see https://github.com/poolsideai/pool
|
|
71385
|
-
*/
|
|
71386
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
71387
|
-
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
71388
|
-
//#endregion
|
|
71389
74225
|
//#region src/features/rules/pool-rule.ts
|
|
71390
74226
|
var PoolRule = class PoolRule extends ToolRule {
|
|
71391
74227
|
static getSettablePaths({ global = false } = {}) {
|
|
@@ -72958,6 +75794,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
72958
75794
|
collisionPolicy: "fold"
|
|
72959
75795
|
}
|
|
72960
75796
|
}],
|
|
75797
|
+
["commandcode", {
|
|
75798
|
+
class: CommandcodeRule,
|
|
75799
|
+
meta: {
|
|
75800
|
+
extension: "md",
|
|
75801
|
+
supportsGlobal: true,
|
|
75802
|
+
ruleDiscoveryMode: "auto",
|
|
75803
|
+
collisionPolicy: "fold"
|
|
75804
|
+
}
|
|
75805
|
+
}],
|
|
72961
75806
|
["continue", {
|
|
72962
75807
|
class: ContinueRule,
|
|
72963
75808
|
meta: {
|