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
|
@@ -478,6 +478,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
478
478
|
"cline",
|
|
479
479
|
"codebuddy",
|
|
480
480
|
"codexcli",
|
|
481
|
+
"commandcode",
|
|
481
482
|
"continue",
|
|
482
483
|
"copilot",
|
|
483
484
|
"copilotcli",
|
|
@@ -554,10 +555,12 @@ const mcpProcessorToolTargetTuple = [
|
|
|
554
555
|
"claudecode-legacy",
|
|
555
556
|
"cline",
|
|
556
557
|
"codexcli",
|
|
558
|
+
"commandcode",
|
|
557
559
|
"continue",
|
|
558
560
|
"copilot",
|
|
559
561
|
"copilotcli",
|
|
560
562
|
"cortexcode",
|
|
563
|
+
"crush",
|
|
561
564
|
"cursor",
|
|
562
565
|
"deepagents",
|
|
563
566
|
"factorydroid",
|
|
@@ -596,6 +599,7 @@ const commandsProcessorToolTargetTuple = [
|
|
|
596
599
|
"claudecode-legacy",
|
|
597
600
|
"cline",
|
|
598
601
|
"codexcli",
|
|
602
|
+
"commandcode",
|
|
599
603
|
"continue",
|
|
600
604
|
"copilot",
|
|
601
605
|
"cursor",
|
|
@@ -633,6 +637,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
633
637
|
"claudecode-legacy",
|
|
634
638
|
"cline",
|
|
635
639
|
"codexcli",
|
|
640
|
+
"commandcode",
|
|
636
641
|
"copilot",
|
|
637
642
|
"copilotcli",
|
|
638
643
|
"cortexcode",
|
|
@@ -674,6 +679,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
674
679
|
"claudecode-legacy",
|
|
675
680
|
"cline",
|
|
676
681
|
"codexcli",
|
|
682
|
+
"commandcode",
|
|
677
683
|
"continue",
|
|
678
684
|
"copilot",
|
|
679
685
|
"copilotcli",
|
|
@@ -694,6 +700,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
694
700
|
"musecode",
|
|
695
701
|
"opencode",
|
|
696
702
|
"pi",
|
|
703
|
+
"pool",
|
|
697
704
|
"qwencode",
|
|
698
705
|
"reasonix",
|
|
699
706
|
"replit",
|
|
@@ -720,10 +727,12 @@ const hooksProcessorToolTargetTuple = [
|
|
|
720
727
|
"claudecode",
|
|
721
728
|
"claudecode-plugin",
|
|
722
729
|
"codexcli",
|
|
730
|
+
"commandcode",
|
|
723
731
|
"continue",
|
|
724
732
|
"copilot",
|
|
725
733
|
"copilotcli",
|
|
726
734
|
"cortexcode",
|
|
735
|
+
"crush",
|
|
727
736
|
"opencode",
|
|
728
737
|
"pi",
|
|
729
738
|
"factorydroid",
|
|
@@ -753,9 +762,11 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
753
762
|
"claudecode",
|
|
754
763
|
"cline",
|
|
755
764
|
"codexcli",
|
|
765
|
+
"commandcode",
|
|
756
766
|
"continue",
|
|
757
767
|
"copilot",
|
|
758
768
|
"copilotcli",
|
|
769
|
+
"crush",
|
|
759
770
|
"cursor",
|
|
760
771
|
"deepagents",
|
|
761
772
|
"devin",
|
|
@@ -3266,6 +3277,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3266
3277
|
".claude/settings.json",
|
|
3267
3278
|
".claude/settings.local.json",
|
|
3268
3279
|
".codex/config.toml",
|
|
3280
|
+
".commandcode/settings.json",
|
|
3269
3281
|
".continue/settings.json",
|
|
3270
3282
|
".copilot/settings.json",
|
|
3271
3283
|
".github/copilot/settings.json",
|
|
@@ -3283,6 +3295,9 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3283
3295
|
"reasonix.toml",
|
|
3284
3296
|
".vscode/settings.json",
|
|
3285
3297
|
".zed/settings.json",
|
|
3298
|
+
"crush.json",
|
|
3299
|
+
".crush.json",
|
|
3300
|
+
".config/crush/crush.json",
|
|
3286
3301
|
"kilo.json",
|
|
3287
3302
|
"kilo.jsonc",
|
|
3288
3303
|
"opencode.json",
|
|
@@ -3487,6 +3502,27 @@ function omitPrototypePollutionKeys(record) {
|
|
|
3487
3502
|
}
|
|
3488
3503
|
return sanitized;
|
|
3489
3504
|
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
|
|
3507
|
+
* reachable through the value (directly, or through an array) is copied with
|
|
3508
|
+
* its prototype-pollution keys dropped. Scalars and arrays of scalars come back
|
|
3509
|
+
* as they are.
|
|
3510
|
+
*
|
|
3511
|
+
* Use when passing an unknown, user-supplied value through unchanged — an MCP
|
|
3512
|
+
* server field a tool documents that rulesync does not model — so a
|
|
3513
|
+
* `__proto__` key nested anywhere inside it cannot ride into the generated
|
|
3514
|
+
* config, where the consuming tool may merge the object without the same care.
|
|
3515
|
+
*/
|
|
3516
|
+
function omitPrototypePollutionKeysDeep(value) {
|
|
3517
|
+
if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
|
|
3518
|
+
if (value === null || typeof value !== "object") return value;
|
|
3519
|
+
const sanitized = {};
|
|
3520
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3521
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3522
|
+
sanitized[key] = omitPrototypePollutionKeysDeep(entry);
|
|
3523
|
+
}
|
|
3524
|
+
return sanitized;
|
|
3525
|
+
}
|
|
3490
3526
|
//#endregion
|
|
3491
3527
|
//#region src/utils/type-guards.ts
|
|
3492
3528
|
/**
|
|
@@ -4554,6 +4590,14 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4554
4590
|
"stop"
|
|
4555
4591
|
];
|
|
4556
4592
|
/**
|
|
4593
|
+
* Hook events supported by Crush.
|
|
4594
|
+
*
|
|
4595
|
+
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
4596
|
+
* (Claude Code-compatible payload; `matcher` is a regex on the tool name).
|
|
4597
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
4598
|
+
*/
|
|
4599
|
+
const CRUSH_HOOK_EVENTS = ["preToolUse"];
|
|
4600
|
+
/**
|
|
4557
4601
|
* Hook events supported by JetBrains Junie CLI.
|
|
4558
4602
|
*
|
|
4559
4603
|
* Junie CLI exposes seven lifecycle events under the `"hooks"` key of
|
|
@@ -4891,6 +4935,30 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
|
|
|
4891
4935
|
};
|
|
4892
4936
|
const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
4893
4937
|
/**
|
|
4938
|
+
* Hook events supported by Command Code.
|
|
4939
|
+
*
|
|
4940
|
+
* Command Code reads hooks from the `hooks` key of `.commandcode/settings.json`
|
|
4941
|
+
* (project) and `~/.commandcode/settings.json` (user) in the Claude-Code shape.
|
|
4942
|
+
* Four events are documented: PreToolUse, PostToolUse, Stop and SessionStart.
|
|
4943
|
+
* Only `command` hooks exist; `timeout` is in seconds (default 30, max 600)
|
|
4944
|
+
* and `$COMMANDCODE_PROJECT_DIR` resolves to the project root.
|
|
4945
|
+
*
|
|
4946
|
+
* @see https://commandcode.ai/docs/hooks
|
|
4947
|
+
*/
|
|
4948
|
+
const COMMANDCODE_HOOK_EVENTS = [
|
|
4949
|
+
"preToolUse",
|
|
4950
|
+
"postToolUse",
|
|
4951
|
+
"stop",
|
|
4952
|
+
"sessionStart"
|
|
4953
|
+
];
|
|
4954
|
+
const CANONICAL_TO_COMMANDCODE_EVENT_NAMES = {
|
|
4955
|
+
preToolUse: "PreToolUse",
|
|
4956
|
+
postToolUse: "PostToolUse",
|
|
4957
|
+
stop: "Stop",
|
|
4958
|
+
sessionStart: "SessionStart"
|
|
4959
|
+
};
|
|
4960
|
+
const COMMANDCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_COMMANDCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
4961
|
+
/**
|
|
4894
4962
|
* Hook events supported by the Continue CLI (`cn`).
|
|
4895
4963
|
*
|
|
4896
4964
|
* Continue reads a Claude-Code-compatible `hooks` key from
|
|
@@ -5096,8 +5164,10 @@ const HooksConfigSchema = z.looseObject({
|
|
|
5096
5164
|
grokcli: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5097
5165
|
"kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5098
5166
|
zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5167
|
+
crush: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5099
5168
|
bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5100
5169
|
cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5170
|
+
commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5101
5171
|
continue: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5102
5172
|
tabnine: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5103
5173
|
qwencode: z.optional(z.looseObject({
|
|
@@ -5593,6 +5663,13 @@ const VIBE_TO_CANONICAL_EVENT_NAMES = {
|
|
|
5593
5663
|
post_agent_turn: "stop"
|
|
5594
5664
|
};
|
|
5595
5665
|
/**
|
|
5666
|
+
* Canonical -> Crush event names. Crush spells its events the Claude Code
|
|
5667
|
+
* way (`PreToolUse`).
|
|
5668
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
5669
|
+
*/
|
|
5670
|
+
const CANONICAL_TO_CRUSH_EVENT_NAMES = { preToolUse: "PreToolUse" };
|
|
5671
|
+
const CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5672
|
+
/**
|
|
5596
5673
|
* Map canonical camelCase event names to Qwen Code PascalCase.
|
|
5597
5674
|
*
|
|
5598
5675
|
* Qwen Code reuses the same Claude-style PascalCase event names for the events
|
|
@@ -6032,6 +6109,11 @@ const McpServerSchema = z.looseObject({
|
|
|
6032
6109
|
kiroAutoBlock: z.optional(z.array(z.string())),
|
|
6033
6110
|
musecodeMode: z.optional(z.enum(["required", "optional"])),
|
|
6034
6111
|
rovodevEnableInstructions: z.optional(z.boolean()),
|
|
6112
|
+
crushOauth: z.optional(z.boolean()),
|
|
6113
|
+
crushOauthClientId: z.optional(z.string()),
|
|
6114
|
+
crushOauthClientSecret: z.optional(z.string()),
|
|
6115
|
+
crushOauthCallbackPort: z.optional(z.int()),
|
|
6116
|
+
crushSessionless: z.optional(z.boolean()),
|
|
6035
6117
|
headers: z.optional(z.record(z.string(), z.string())),
|
|
6036
6118
|
/**
|
|
6037
6119
|
* The canonical per-server tool allowlist.
|
|
@@ -6105,9 +6187,12 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
6105
6187
|
claudecode: z.optional(toolScopedMcpSchema),
|
|
6106
6188
|
cline: z.optional(toolScopedMcpSchema),
|
|
6107
6189
|
codexcli: z.optional(toolScopedMcpSchema),
|
|
6190
|
+
commandcode: z.optional(toolScopedMcpSchema),
|
|
6108
6191
|
continue: z.optional(toolScopedMcpSchema),
|
|
6109
6192
|
copilot: z.optional(toolScopedMcpSchema),
|
|
6110
6193
|
copilotcli: z.optional(toolScopedMcpSchema),
|
|
6194
|
+
cortexcode: z.optional(toolScopedMcpSchema),
|
|
6195
|
+
crush: z.optional(toolScopedMcpSchema),
|
|
6111
6196
|
cursor: z.optional(toolScopedMcpSchema),
|
|
6112
6197
|
deepagents: z.optional(toolScopedMcpSchema),
|
|
6113
6198
|
devin: z.optional(toolScopedMcpSchema),
|
|
@@ -6124,7 +6209,6 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
6124
6209
|
reasonix: z.optional(toolScopedMcpSchema),
|
|
6125
6210
|
roo: z.optional(toolScopedMcpSchema),
|
|
6126
6211
|
rovodev: z.optional(toolScopedMcpSchema),
|
|
6127
|
-
cortexcode: z.optional(toolScopedMcpSchema),
|
|
6128
6212
|
tabnine: z.optional(toolScopedMcpSchema),
|
|
6129
6213
|
takt: z.optional(toolScopedMcpSchema),
|
|
6130
6214
|
vibe: z.optional(toolScopedMcpSchema),
|
|
@@ -6456,6 +6540,15 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6456
6540
|
"musecodeMode",
|
|
6457
6541
|
"rovodevEnableInstructions",
|
|
6458
6542
|
"enable_instructions",
|
|
6543
|
+
"crushOauth",
|
|
6544
|
+
"crushOauthClientId",
|
|
6545
|
+
"crushOauthClientSecret",
|
|
6546
|
+
"crushOauthCallbackPort",
|
|
6547
|
+
"crushSessionless",
|
|
6548
|
+
"oauth_client_id",
|
|
6549
|
+
"oauth_client_secret",
|
|
6550
|
+
"oauth_callback_port",
|
|
6551
|
+
"sessionless",
|
|
6459
6552
|
"enabled"
|
|
6460
6553
|
])];
|
|
6461
6554
|
}));
|
|
@@ -6490,12 +6583,16 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6490
6583
|
* Targets that share one output file resolve identically so the shared
|
|
6491
6584
|
* file's content never depends on which of them generates last — see
|
|
6492
6585
|
* `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
|
|
6493
|
-
* and the
|
|
6586
|
+
* the Antigravity pair, and the project-scope Claude Code / Command Code
|
|
6587
|
+
* pair, which is why the scope is part of the resolution).
|
|
6494
6588
|
*
|
|
6495
6589
|
* Returns the same instance when neither mechanism is used.
|
|
6496
6590
|
*/
|
|
6497
|
-
forTarget({ toolTarget, logger }) {
|
|
6498
|
-
const { blockKeys, acceptedTargetNames } = resolveMcpTarget({
|
|
6591
|
+
forTarget({ toolTarget, global = false, logger }) {
|
|
6592
|
+
const { blockKeys, acceptedTargetNames } = resolveMcpTarget({
|
|
6593
|
+
toolTarget,
|
|
6594
|
+
global
|
|
6595
|
+
});
|
|
6499
6596
|
const json = this.json;
|
|
6500
6597
|
const sharedServers = this.json.mcpServers ?? {};
|
|
6501
6598
|
const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
|
|
@@ -6582,9 +6679,24 @@ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
|
|
|
6582
6679
|
* `config`) — so both targets always apply both blocks in a fixed order
|
|
6583
6680
|
* (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
|
|
6584
6681
|
* per server on conflict).
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6682
|
+
* - `claudecode` (and its legacy alias) and `commandcode` share the root
|
|
6683
|
+
* `.mcp.json` in PROJECT mode only (their global files differ:
|
|
6684
|
+
* `~/.claude.json` vs `~/.commandcode/mcp.json`), so in project mode the
|
|
6685
|
+
* three apply the `claudecode` and `commandcode` blocks in that fixed order
|
|
6686
|
+
* (the `commandcode` block wins per server on conflict); in global mode
|
|
6687
|
+
* each reads its own block.
|
|
6688
|
+
*/
|
|
6689
|
+
function resolveMcpTarget({ toolTarget, global }) {
|
|
6690
|
+
const isClaudecode = toolTarget === "claudecode" || toolTarget === "claudecode-legacy";
|
|
6691
|
+
if (!global && (isClaudecode || toolTarget === "commandcode")) return {
|
|
6692
|
+
blockKeys: ["claudecode", "commandcode"],
|
|
6693
|
+
acceptedTargetNames: /* @__PURE__ */ new Set([
|
|
6694
|
+
"claudecode",
|
|
6695
|
+
"claudecode-legacy",
|
|
6696
|
+
"commandcode"
|
|
6697
|
+
])
|
|
6698
|
+
};
|
|
6699
|
+
if (isClaudecode) return {
|
|
6588
6700
|
blockKeys: ["claudecode"],
|
|
6589
6701
|
acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
|
|
6590
6702
|
};
|
|
@@ -6904,7 +7016,7 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
6904
7016
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
6905
7017
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
6906
7018
|
* `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
|
|
6907
|
-
* `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
7019
|
+
* `todoWrite`, `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
6908
7020
|
* `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
|
|
6909
7021
|
* `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
|
|
6910
7022
|
* `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
|
|
@@ -6917,7 +7029,7 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
6917
7029
|
* changes what the file said is reported in either scope, naming what that key
|
|
6918
7030
|
* decides there — the autonomy and containment
|
|
6919
7031
|
* controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
|
|
6920
|
-
* registry controls (`disabled`, `visible`, `listDirectory`, and `eager`, which
|
|
7032
|
+
* registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
|
|
6921
7033
|
* demotes an omitted tool to deferred rather than removing it, replacing rather
|
|
6922
7034
|
* than merging with the list a higher scope set), the Auto Mode
|
|
6923
7035
|
* classifier config, and, because these groups are loose objects, any key
|
|
@@ -6958,6 +7070,14 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
|
6958
7070
|
* `looseObject` (verbatim passthrough). Both project and global scope are
|
|
6959
7071
|
* supported.
|
|
6960
7072
|
*
|
|
7073
|
+
* Only `tools` and `general` are written; any other key is ignored with a
|
|
7074
|
+
* warning. Within them, the paths Tabnine CLI runs as a command or sends its
|
|
7075
|
+
* traffic to — `tools.discoveryCommand`, `tools.callCommand`,
|
|
7076
|
+
* `tools.shell.pager`, `tools.sandbox.command`, `general.tabnineHost` and
|
|
7077
|
+
* `general.preferredEditor` — are refused with a warning rather than written,
|
|
7078
|
+
* so a fetched permissions file cannot point the CLI at an executable or a
|
|
7079
|
+
* server of its choosing; set those by hand in `settings.json`.
|
|
7080
|
+
*
|
|
6961
7081
|
* @example
|
|
6962
7082
|
* { "tools": { "core": ["read_file", "run_shell_command(git)"] },
|
|
6963
7083
|
* "general": { "defaultApprovalMode": "plan" } }
|
|
@@ -7741,7 +7861,9 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
7741
7861
|
"antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
|
|
7742
7862
|
continue: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7743
7863
|
copilot: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7864
|
+
commandcode: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7744
7865
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7866
|
+
crush: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7745
7867
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7746
7868
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7747
7869
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
@@ -9462,6 +9584,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
9462
9584
|
"user-invocable": z.optional(z.boolean()),
|
|
9463
9585
|
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
9464
9586
|
})),
|
|
9587
|
+
commandcode: z.optional(z.looseObject({})),
|
|
9465
9588
|
cortexcode: z.optional(z.looseObject({})),
|
|
9466
9589
|
continue: z.optional(z.looseObject({})),
|
|
9467
9590
|
tabnine: z.optional(z.looseObject({})),
|
|
@@ -12767,6 +12890,26 @@ const ZCODE_USER_CONFIG_DECLARATION = {
|
|
|
12767
12890
|
}
|
|
12768
12891
|
};
|
|
12769
12892
|
/**
|
|
12893
|
+
* Crush's JSON config, shared by the project (`crush.json` / `.crush.json`)
|
|
12894
|
+
* and global (`~/.config/crush/crush.json`) spellings so a policy edit lands
|
|
12895
|
+
* on both. See the `SHARED_CONFIG_OWNERSHIP` entries for the key rationale.
|
|
12896
|
+
*/
|
|
12897
|
+
const CRUSH_CONFIG_DECLARATION = {
|
|
12898
|
+
format: "json",
|
|
12899
|
+
invalidRootPolicy: "error",
|
|
12900
|
+
features: {
|
|
12901
|
+
mcp: {
|
|
12902
|
+
kind: "replace-owned-keys",
|
|
12903
|
+
ownedKeys: ["mcp"]
|
|
12904
|
+
},
|
|
12905
|
+
hooks: {
|
|
12906
|
+
kind: "replace-owned-keys",
|
|
12907
|
+
ownedKeys: ["hooks"]
|
|
12908
|
+
},
|
|
12909
|
+
permissions: { kind: "deep-merge" }
|
|
12910
|
+
}
|
|
12911
|
+
};
|
|
12912
|
+
/**
|
|
12770
12913
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
12771
12914
|
* and both validate against the one published schema, which the gateway
|
|
12772
12915
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13027,6 +13170,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13027
13170
|
},
|
|
13028
13171
|
".zcode/config.json": ZCODE_WORKSPACE_CONFIG_DECLARATION,
|
|
13029
13172
|
".zcode/cli/config.json": ZCODE_USER_CONFIG_DECLARATION,
|
|
13173
|
+
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13174
|
+
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13030
13175
|
".bob/settings.json": {
|
|
13031
13176
|
format: "json",
|
|
13032
13177
|
invalidRootPolicy: "error",
|
|
@@ -13051,6 +13196,20 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13051
13196
|
ownedKeys: ["hooks"]
|
|
13052
13197
|
} }
|
|
13053
13198
|
},
|
|
13199
|
+
".commandcode/settings.json": {
|
|
13200
|
+
format: "json",
|
|
13201
|
+
invalidRootPolicy: "error",
|
|
13202
|
+
features: {
|
|
13203
|
+
hooks: {
|
|
13204
|
+
kind: "replace-owned-keys",
|
|
13205
|
+
ownedKeys: ["hooks"]
|
|
13206
|
+
},
|
|
13207
|
+
permissions: {
|
|
13208
|
+
kind: "replace-owned-keys",
|
|
13209
|
+
ownedKeys: ["permissions"]
|
|
13210
|
+
}
|
|
13211
|
+
}
|
|
13212
|
+
},
|
|
13054
13213
|
".cortex/settings.json": {
|
|
13055
13214
|
format: "json",
|
|
13056
13215
|
invalidRootPolicy: "error",
|
|
@@ -15252,6 +15411,94 @@ var CodexcliCommand = class CodexcliCommand extends ToolCommand {
|
|
|
15252
15411
|
}
|
|
15253
15412
|
};
|
|
15254
15413
|
//#endregion
|
|
15414
|
+
//#region src/constants/commandcode-paths.ts
|
|
15415
|
+
const COMMANDCODE_DIR = ".commandcode";
|
|
15416
|
+
const COMMANDCODE_RULE_FILE_NAME = "AGENTS.md";
|
|
15417
|
+
const COMMANDCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
15418
|
+
const COMMANDCODE_PROJECT_MCP_FILE_NAME = ".mcp.json";
|
|
15419
|
+
const COMMANDCODE_GLOBAL_MCP_FILE_NAME = "mcp.json";
|
|
15420
|
+
const COMMANDCODE_COMMANDS_DIR_PATH = join(COMMANDCODE_DIR, "commands");
|
|
15421
|
+
const COMMANDCODE_AGENTS_DIR_PATH = join(COMMANDCODE_DIR, "agents");
|
|
15422
|
+
const COMMANDCODE_SKILLS_DIR_PATH = join(COMMANDCODE_DIR, "skills");
|
|
15423
|
+
//#endregion
|
|
15424
|
+
//#region src/features/commands/commandcode-command.ts
|
|
15425
|
+
/**
|
|
15426
|
+
* Custom slash command for Command Code.
|
|
15427
|
+
*
|
|
15428
|
+
* Command Code reads Markdown files from `.commandcode/commands/` (project)
|
|
15429
|
+
* and `~/.commandcode/commands/` (user), naming each command after the file's
|
|
15430
|
+
* basename; subdirectories only group the files. The file's full trimmed
|
|
15431
|
+
* body is the prompt that runs — YAML frontmatter is not stripped, only
|
|
15432
|
+
* skipped when the slash menu picks a summary line — so rulesync writes the
|
|
15433
|
+
* bare body. On import a hand-written file's frontmatter block is dropped
|
|
15434
|
+
* (Command Code would send it as prompt text; a rulesync command carries its
|
|
15435
|
+
* own frontmatter) and only the body is kept.
|
|
15436
|
+
*
|
|
15437
|
+
* @see https://commandcode.ai/docs/custom-slash-commands
|
|
15438
|
+
*/
|
|
15439
|
+
var CommandcodeCommand = class CommandcodeCommand extends ToolCommand {
|
|
15440
|
+
static getSettablePaths(_options = {}) {
|
|
15441
|
+
return { relativeDirPath: COMMANDCODE_COMMANDS_DIR_PATH };
|
|
15442
|
+
}
|
|
15443
|
+
toRulesyncCommand() {
|
|
15444
|
+
return new RulesyncCommand({
|
|
15445
|
+
outputRoot: process.cwd(),
|
|
15446
|
+
frontmatter: { targets: ["*"] },
|
|
15447
|
+
body: this.getFileContent(),
|
|
15448
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
15449
|
+
relativeFilePath: this.relativeFilePath,
|
|
15450
|
+
fileContent: this.getFileContent(),
|
|
15451
|
+
validate: true
|
|
15452
|
+
});
|
|
15453
|
+
}
|
|
15454
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true }) {
|
|
15455
|
+
const paths = this.getSettablePaths();
|
|
15456
|
+
return new CommandcodeCommand({
|
|
15457
|
+
outputRoot,
|
|
15458
|
+
fileContent: rulesyncCommand.getBody(),
|
|
15459
|
+
relativeDirPath: paths.relativeDirPath,
|
|
15460
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
15461
|
+
validate
|
|
15462
|
+
});
|
|
15463
|
+
}
|
|
15464
|
+
validate() {
|
|
15465
|
+
return {
|
|
15466
|
+
success: true,
|
|
15467
|
+
error: null
|
|
15468
|
+
};
|
|
15469
|
+
}
|
|
15470
|
+
getBody() {
|
|
15471
|
+
return this.getFileContent();
|
|
15472
|
+
}
|
|
15473
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
15474
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
15475
|
+
rulesyncCommand,
|
|
15476
|
+
toolTarget: "commandcode"
|
|
15477
|
+
});
|
|
15478
|
+
}
|
|
15479
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
15480
|
+
const paths = this.getSettablePaths();
|
|
15481
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
15482
|
+
const { body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
15483
|
+
return new CommandcodeCommand({
|
|
15484
|
+
outputRoot,
|
|
15485
|
+
relativeDirPath: paths.relativeDirPath,
|
|
15486
|
+
relativeFilePath,
|
|
15487
|
+
fileContent: content.trim(),
|
|
15488
|
+
validate
|
|
15489
|
+
});
|
|
15490
|
+
}
|
|
15491
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
15492
|
+
return new CommandcodeCommand({
|
|
15493
|
+
outputRoot,
|
|
15494
|
+
relativeDirPath,
|
|
15495
|
+
relativeFilePath,
|
|
15496
|
+
fileContent: "",
|
|
15497
|
+
validate: false
|
|
15498
|
+
});
|
|
15499
|
+
}
|
|
15500
|
+
};
|
|
15501
|
+
//#endregion
|
|
15255
15502
|
//#region src/constants/continue-paths.ts
|
|
15256
15503
|
const CONTINUE_DIR = ".continue";
|
|
15257
15504
|
const CONTINUE_ROOT_RULE_FILE_NAME = "AGENTS.md";
|
|
@@ -18876,15 +19123,12 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
|
|
|
18876
19123
|
return this.body;
|
|
18877
19124
|
}
|
|
18878
19125
|
getFrontmatter() {
|
|
18879
|
-
return
|
|
18880
|
-
description: this.frontmatter.description,
|
|
18881
|
-
prompt: this.frontmatter.prompt
|
|
18882
|
-
};
|
|
19126
|
+
return this.frontmatter;
|
|
18883
19127
|
}
|
|
18884
19128
|
toRulesyncCommand() {
|
|
18885
19129
|
const { description, prompt: _prompt, ...restFields } = this.frontmatter;
|
|
18886
19130
|
const rulesyncFrontmatter = {
|
|
18887
|
-
targets: ["
|
|
19131
|
+
targets: ["*"],
|
|
18888
19132
|
description,
|
|
18889
19133
|
...Object.keys(restFields).length > 0 && { tabnine: restFields }
|
|
18890
19134
|
};
|
|
@@ -19498,6 +19742,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
19498
19742
|
supportsSubdirectory: false
|
|
19499
19743
|
}
|
|
19500
19744
|
}],
|
|
19745
|
+
["commandcode", {
|
|
19746
|
+
class: CommandcodeCommand,
|
|
19747
|
+
meta: {
|
|
19748
|
+
extension: "md",
|
|
19749
|
+
supportsProject: true,
|
|
19750
|
+
supportsGlobal: true,
|
|
19751
|
+
isSimulated: false,
|
|
19752
|
+
supportsSubdirectory: true
|
|
19753
|
+
}
|
|
19754
|
+
}],
|
|
19501
19755
|
["continue", {
|
|
19502
19756
|
class: ContinueCommand,
|
|
19503
19757
|
meta: {
|
|
@@ -20847,7 +21101,20 @@ function stableJson(value) {
|
|
|
20847
21101
|
*/
|
|
20848
21102
|
function isSafeStringRecord(value) {
|
|
20849
21103
|
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
20850
|
-
return Object.entries(value).every(([key, entry]) =>
|
|
21104
|
+
return Object.entries(value).every(([key, entry]) => isSafeEnvEntry({
|
|
21105
|
+
key,
|
|
21106
|
+
value: entry
|
|
21107
|
+
}));
|
|
21108
|
+
}
|
|
21109
|
+
/**
|
|
21110
|
+
* Whether one `env` entry survives the rule {@link isSafeStringRecord} applies
|
|
21111
|
+
* to the whole map. Exported for the adapters that hand a tool an environment
|
|
21112
|
+
* block outside the shared converter (Tabnine, whose hook shape it does not
|
|
21113
|
+
* express) so they drop the same entries this one does.
|
|
21114
|
+
*/
|
|
21115
|
+
function isSafeEnvEntry(entry) {
|
|
21116
|
+
const { key, value } = entry;
|
|
21117
|
+
return typeof value === "string" && key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || value.includes(char));
|
|
20851
21118
|
}
|
|
20852
21119
|
/**
|
|
20853
21120
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
@@ -21738,135 +22005,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
21738
22005
|
}
|
|
21739
22006
|
};
|
|
21740
22007
|
//#endregion
|
|
21741
|
-
//#region src/features/hooks/bob-hooks.ts
|
|
21742
|
-
const BOB_CONVERTER_CONFIG = {
|
|
21743
|
-
supportedEvents: BOB_HOOK_EVENTS,
|
|
21744
|
-
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
21745
|
-
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
21746
|
-
projectDirVar: "",
|
|
21747
|
-
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
21748
|
-
"sessionStart",
|
|
21749
|
-
"beforeSubmitPrompt",
|
|
21750
|
-
"stop"
|
|
21751
|
-
]),
|
|
21752
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
21753
|
-
wildcardMatcherMeansAll: true
|
|
21754
|
-
};
|
|
21755
|
-
/**
|
|
21756
|
-
* Single spelling of the settings.json codec/policy: fail closed on an
|
|
21757
|
-
* unparseable root rather than replacing the user's Bob settings with
|
|
21758
|
-
* generated output.
|
|
21759
|
-
*/
|
|
21760
|
-
function parseBobSettings(fileContent, filePath) {
|
|
21761
|
-
return parseSharedConfig({
|
|
21762
|
-
format: "json",
|
|
21763
|
-
fileContent,
|
|
21764
|
-
filePath,
|
|
21765
|
-
invalidRootPolicy: "error"
|
|
21766
|
-
});
|
|
21767
|
-
}
|
|
21768
|
-
/**
|
|
21769
|
-
* IBM Bob lifecycle hooks.
|
|
21770
|
-
*
|
|
21771
|
-
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
21772
|
-
* `<project>/.bob/settings.json` (project scope) and
|
|
21773
|
-
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
21774
|
-
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
21775
|
-
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
21776
|
-
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
21777
|
-
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
21778
|
-
*
|
|
21779
|
-
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
21780
|
-
*/
|
|
21781
|
-
var BobHooks = class BobHooks extends ToolHooks {
|
|
21782
|
-
constructor(params) {
|
|
21783
|
-
super({
|
|
21784
|
-
...params,
|
|
21785
|
-
fileContent: params.fileContent ?? "{}"
|
|
21786
|
-
});
|
|
21787
|
-
}
|
|
21788
|
-
isDeletable() {
|
|
21789
|
-
return false;
|
|
21790
|
-
}
|
|
21791
|
-
static getSettablePaths({ global = false } = {}) {
|
|
21792
|
-
return {
|
|
21793
|
-
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
21794
|
-
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
21795
|
-
};
|
|
21796
|
-
}
|
|
21797
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
21798
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21799
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
21800
|
-
return new BobHooks({
|
|
21801
|
-
outputRoot,
|
|
21802
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21803
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21804
|
-
fileContent,
|
|
21805
|
-
validate
|
|
21806
|
-
});
|
|
21807
|
-
}
|
|
21808
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
21809
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21810
|
-
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21811
|
-
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
21812
|
-
const config = rulesyncHooks.getJson();
|
|
21813
|
-
const hooks = canonicalToToolHooks({
|
|
21814
|
-
config,
|
|
21815
|
-
toolOverrideHooks: config.bob?.hooks,
|
|
21816
|
-
converterConfig: BOB_CONVERTER_CONFIG,
|
|
21817
|
-
logger
|
|
21818
|
-
});
|
|
21819
|
-
const fileContent = applySharedConfigPatch({
|
|
21820
|
-
fileKey: sharedConfigFileKey(paths),
|
|
21821
|
-
feature: "hooks",
|
|
21822
|
-
existingContent,
|
|
21823
|
-
patch: { hooks },
|
|
21824
|
-
filePath,
|
|
21825
|
-
logger
|
|
21826
|
-
});
|
|
21827
|
-
return new BobHooks({
|
|
21828
|
-
outputRoot,
|
|
21829
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21830
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21831
|
-
fileContent,
|
|
21832
|
-
validate
|
|
21833
|
-
});
|
|
21834
|
-
}
|
|
21835
|
-
toRulesyncHooks({ logger } = {}) {
|
|
21836
|
-
const configPath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
21837
|
-
let settings;
|
|
21838
|
-
try {
|
|
21839
|
-
settings = parseBobSettings(this.getFileContent(), configPath);
|
|
21840
|
-
} catch (error) {
|
|
21841
|
-
throw new Error(`Failed to parse Bob hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
21842
|
-
}
|
|
21843
|
-
const hooks = toolHooksToCanonical({
|
|
21844
|
-
logger,
|
|
21845
|
-
hooks: settings.hooks,
|
|
21846
|
-
converterConfig: BOB_CONVERTER_CONFIG
|
|
21847
|
-
});
|
|
21848
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
21849
|
-
hooks,
|
|
21850
|
-
overrideKey: "bob"
|
|
21851
|
-
}), null, 2) });
|
|
21852
|
-
}
|
|
21853
|
-
validate() {
|
|
21854
|
-
return {
|
|
21855
|
-
success: true,
|
|
21856
|
-
error: null
|
|
21857
|
-
};
|
|
21858
|
-
}
|
|
21859
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
21860
|
-
return new BobHooks({
|
|
21861
|
-
outputRoot,
|
|
21862
|
-
relativeDirPath,
|
|
21863
|
-
relativeFilePath,
|
|
21864
|
-
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
21865
|
-
validate: false
|
|
21866
|
-
});
|
|
21867
|
-
}
|
|
21868
|
-
};
|
|
21869
|
-
//#endregion
|
|
21870
22008
|
//#region src/features/hooks/preserve-unowned-hook-commands.ts
|
|
21871
22009
|
/**
|
|
21872
22010
|
* Read the `hooks` value from a destination JSON file.
|
|
@@ -22153,75 +22291,22 @@ function warnSkip({ logger, event, expected }) {
|
|
|
22153
22291
|
logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
|
|
22154
22292
|
}
|
|
22155
22293
|
//#endregion
|
|
22156
|
-
//#region src/features/hooks/
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
|
|
22160
|
-
|
|
22161
|
-
|
|
22162
|
-
|
|
22163
|
-
|
|
22164
|
-
|
|
22165
|
-
|
|
22166
|
-
|
|
22167
|
-
|
|
22168
|
-
|
|
22169
|
-
|
|
22170
|
-
|
|
22171
|
-
|
|
22172
|
-
"beforeSubmitPrompt",
|
|
22173
|
-
"stop"
|
|
22174
|
-
]),
|
|
22175
|
-
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22176
|
-
"command",
|
|
22177
|
-
"prompt",
|
|
22178
|
-
"http",
|
|
22179
|
-
"mcp_tool",
|
|
22180
|
-
"agent"
|
|
22181
|
-
]),
|
|
22182
|
-
emitsPromptModel: true,
|
|
22183
|
-
stringPassthroughFields: [
|
|
22184
|
-
{
|
|
22185
|
-
canonical: "if",
|
|
22186
|
-
tool: "if"
|
|
22187
|
-
},
|
|
22188
|
-
{
|
|
22189
|
-
canonical: "statusMessage",
|
|
22190
|
-
tool: "statusMessage"
|
|
22191
|
-
},
|
|
22192
|
-
{
|
|
22193
|
-
canonical: "shell",
|
|
22194
|
-
tool: "shell",
|
|
22195
|
-
commandOnly: true
|
|
22196
|
-
}
|
|
22197
|
-
],
|
|
22198
|
-
booleanPassthroughFields: [
|
|
22199
|
-
{
|
|
22200
|
-
canonical: "once",
|
|
22201
|
-
tool: "once"
|
|
22202
|
-
},
|
|
22203
|
-
{
|
|
22204
|
-
canonical: "async",
|
|
22205
|
-
tool: "async",
|
|
22206
|
-
commandOnly: true
|
|
22207
|
-
},
|
|
22208
|
-
{
|
|
22209
|
-
canonical: "asyncRewake",
|
|
22210
|
-
tool: "asyncRewake",
|
|
22211
|
-
commandOnly: true
|
|
22212
|
-
},
|
|
22213
|
-
{
|
|
22214
|
-
canonical: "continueOnBlock",
|
|
22215
|
-
tool: "continueOnBlock"
|
|
22216
|
-
}
|
|
22217
|
-
],
|
|
22218
|
-
arrayPassthroughFields: [{
|
|
22219
|
-
canonical: "args",
|
|
22220
|
-
tool: "args",
|
|
22221
|
-
commandOnly: true
|
|
22222
|
-
}]
|
|
22223
|
-
};
|
|
22224
|
-
var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
22294
|
+
//#region src/features/hooks/settings-json-hooks.ts
|
|
22295
|
+
/**
|
|
22296
|
+
* Shared implementation for the tools whose hooks live under the top-level
|
|
22297
|
+
* `hooks` key of a JSON settings file in Claude Code's shape —
|
|
22298
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command", ... }] }] }`
|
|
22299
|
+
* — and whose file also holds settings rulesync does not own, so generation
|
|
22300
|
+
* merges the `hooks` key into it (see `SHARED_CONFIG_OWNERSHIP`) instead of
|
|
22301
|
+
* overwriting it, and `--delete` never removes the file wholesale.
|
|
22302
|
+
*
|
|
22303
|
+
* A concrete adapter supplies its {@link SettingsJsonHooksSpec} and its
|
|
22304
|
+
* settable paths; the event mapping, the matcher rules and the per-hook
|
|
22305
|
+
* fields the tool documents are all expressed in the spec's converter config.
|
|
22306
|
+
* An adapter that also supports preserving unowned hooks (Claude Code) opts in
|
|
22307
|
+
* through {@link ToolHooks.supportsPreserveUnowned}.
|
|
22308
|
+
*/
|
|
22309
|
+
var SettingsJsonHooks = class extends ToolHooks {
|
|
22225
22310
|
constructor(params) {
|
|
22226
22311
|
super({
|
|
22227
22312
|
...params,
|
|
@@ -22231,24 +22316,42 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22231
22316
|
isDeletable() {
|
|
22232
22317
|
return false;
|
|
22233
22318
|
}
|
|
22319
|
+
/** The tool-specific half of the adapter. Every concrete class overrides this. */
|
|
22320
|
+
static getSpec() {
|
|
22321
|
+
throw new Error(`${this.name} does not define getSpec()`);
|
|
22322
|
+
}
|
|
22234
22323
|
/**
|
|
22235
|
-
* The converter config used for both directions.
|
|
22236
|
-
*
|
|
22237
|
-
*
|
|
22324
|
+
* The converter config used for both directions. A separate hook so a
|
|
22325
|
+
* subclass can swap tool-specific details (e.g. the project directory
|
|
22326
|
+
* variable) without redefining the whole spec.
|
|
22238
22327
|
*/
|
|
22239
22328
|
static getConverterConfig() {
|
|
22240
|
-
return
|
|
22329
|
+
return this.getSpec().converterConfig;
|
|
22241
22330
|
}
|
|
22242
22331
|
static getSettablePaths(_options = {}) {
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22332
|
+
throw new Error(`${this.name} does not define getSettablePaths()`);
|
|
22333
|
+
}
|
|
22334
|
+
/**
|
|
22335
|
+
* The `SHARED_CONFIG_OWNERSHIP` key the settings file is merged under. By
|
|
22336
|
+
* default it is derived from the settable paths; an adapter whose file is
|
|
22337
|
+
* declared under another tool's key (the Claude Code plugin bundle) overrides
|
|
22338
|
+
* it.
|
|
22339
|
+
*/
|
|
22340
|
+
static getSharedFileKey(paths) {
|
|
22341
|
+
return sharedConfigFileKey(paths);
|
|
22342
|
+
}
|
|
22343
|
+
/**
|
|
22344
|
+
* `new this(params)` for the concrete adapter the static method was called
|
|
22345
|
+
* on; the cast is what an abstract class needs to be constructed through
|
|
22346
|
+
* `this`, and lives in one place.
|
|
22347
|
+
*/
|
|
22348
|
+
static instantiate(params) {
|
|
22349
|
+
return new this(params);
|
|
22247
22350
|
}
|
|
22248
22351
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22249
22352
|
const paths = this.getSettablePaths({ global });
|
|
22250
22353
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22251
|
-
return
|
|
22354
|
+
return this.instantiate({
|
|
22252
22355
|
outputRoot,
|
|
22253
22356
|
relativeDirPath: paths.relativeDirPath,
|
|
22254
22357
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22256,40 +22359,41 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22256
22359
|
validate
|
|
22257
22360
|
});
|
|
22258
22361
|
}
|
|
22259
|
-
static supportsPreserveUnowned() {
|
|
22260
|
-
return true;
|
|
22261
|
-
}
|
|
22262
22362
|
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
22263
|
-
return toolHooks instanceof
|
|
22363
|
+
return toolHooks instanceof this ? toolHooks.getOwnershipLockFiles() : [];
|
|
22264
22364
|
}
|
|
22265
22365
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
22266
22366
|
const paths = this.getSettablePaths({ global });
|
|
22267
22367
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22268
22368
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22269
22369
|
const config = rulesyncHooks.getJson();
|
|
22270
|
-
const
|
|
22370
|
+
const generatedHooks = canonicalToToolHooks({
|
|
22271
22371
|
config,
|
|
22272
|
-
toolOverrideHooks:
|
|
22372
|
+
toolOverrideHooks: overrideHooksOf({
|
|
22373
|
+
config,
|
|
22374
|
+
overrideKey: this.getSpec().overrideKey
|
|
22375
|
+
}),
|
|
22273
22376
|
converterConfig: this.getConverterConfig(),
|
|
22274
22377
|
logger
|
|
22275
22378
|
});
|
|
22276
22379
|
const preserving = preserveUnowned && this.supportsPreserveUnowned();
|
|
22277
22380
|
const merged = mergeGeneratedHookLists({
|
|
22278
22381
|
existingContent,
|
|
22279
|
-
generatedHooks
|
|
22382
|
+
generatedHooks,
|
|
22280
22383
|
shape: "matcher-groups",
|
|
22281
22384
|
preserveUnowned: preserving,
|
|
22282
22385
|
previouslyOwned: preserving ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
22283
22386
|
logger
|
|
22284
22387
|
});
|
|
22285
22388
|
const fileContent = applySharedConfigPatch({
|
|
22286
|
-
fileKey:
|
|
22389
|
+
fileKey: this.getSharedFileKey(paths),
|
|
22287
22390
|
feature: "hooks",
|
|
22288
22391
|
existingContent,
|
|
22289
22392
|
patch: { hooks: merged.hooks },
|
|
22290
|
-
filePath
|
|
22393
|
+
filePath,
|
|
22394
|
+
logger
|
|
22291
22395
|
});
|
|
22292
|
-
return
|
|
22396
|
+
return this.instantiate({
|
|
22293
22397
|
outputRoot,
|
|
22294
22398
|
relativeDirPath: paths.relativeDirPath,
|
|
22295
22399
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22299,20 +22403,29 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22299
22403
|
});
|
|
22300
22404
|
}
|
|
22301
22405
|
toRulesyncHooks({ logger } = {}) {
|
|
22406
|
+
const ctor = this.constructor;
|
|
22407
|
+
const spec = ctor.getSpec();
|
|
22408
|
+
const configPath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
22302
22409
|
let settings;
|
|
22303
22410
|
try {
|
|
22304
|
-
settings =
|
|
22411
|
+
settings = parseSharedConfig({
|
|
22412
|
+
format: "json",
|
|
22413
|
+
fileContent: this.getFileContent(),
|
|
22414
|
+
filePath: configPath,
|
|
22415
|
+
invalidRootPolicy: "error"
|
|
22416
|
+
});
|
|
22305
22417
|
} catch (error) {
|
|
22306
|
-
|
|
22418
|
+
const reason = error instanceof Error && error.cause instanceof Error ? error.cause : error;
|
|
22419
|
+
throw new Error(`Failed to parse ${spec.displayName} hooks content in ${configPath}: ${formatError(reason)}`, { cause: error });
|
|
22307
22420
|
}
|
|
22308
22421
|
const hooks = toolHooksToCanonical({
|
|
22422
|
+
logger,
|
|
22309
22423
|
hooks: settings.hooks,
|
|
22310
|
-
converterConfig:
|
|
22311
|
-
logger
|
|
22424
|
+
converterConfig: ctor.getConverterConfig()
|
|
22312
22425
|
});
|
|
22313
22426
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
22314
22427
|
hooks,
|
|
22315
|
-
overrideKey:
|
|
22428
|
+
overrideKey: spec.overrideKey
|
|
22316
22429
|
}), null, 2) });
|
|
22317
22430
|
}
|
|
22318
22431
|
validate() {
|
|
@@ -22322,7 +22435,7 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22322
22435
|
};
|
|
22323
22436
|
}
|
|
22324
22437
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
22325
|
-
return
|
|
22438
|
+
return this.instantiate({
|
|
22326
22439
|
outputRoot,
|
|
22327
22440
|
relativeDirPath,
|
|
22328
22441
|
relativeFilePath,
|
|
@@ -22331,6 +22444,158 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22331
22444
|
});
|
|
22332
22445
|
}
|
|
22333
22446
|
};
|
|
22447
|
+
/**
|
|
22448
|
+
* The `hooks` map of a tool's override block (`config.<overrideKey>.hooks`),
|
|
22449
|
+
* or `undefined` when the block states none. The schema keeps every override
|
|
22450
|
+
* block loose, so the lookup goes through the record form.
|
|
22451
|
+
*/
|
|
22452
|
+
function overrideHooksOf({ config, overrideKey }) {
|
|
22453
|
+
const override = lookupOwn({
|
|
22454
|
+
record: config,
|
|
22455
|
+
key: overrideKey
|
|
22456
|
+
});
|
|
22457
|
+
if (!isPlainObject$1(override)) return;
|
|
22458
|
+
const hooks = override.hooks;
|
|
22459
|
+
return isPlainObject$1(hooks) ? hooks : void 0;
|
|
22460
|
+
}
|
|
22461
|
+
//#endregion
|
|
22462
|
+
//#region src/features/hooks/bob-hooks.ts
|
|
22463
|
+
const BOB_SPEC = {
|
|
22464
|
+
displayName: "Bob",
|
|
22465
|
+
overrideKey: "bob",
|
|
22466
|
+
converterConfig: {
|
|
22467
|
+
supportedEvents: BOB_HOOK_EVENTS,
|
|
22468
|
+
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
22469
|
+
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
22470
|
+
projectDirVar: "",
|
|
22471
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22472
|
+
"sessionStart",
|
|
22473
|
+
"beforeSubmitPrompt",
|
|
22474
|
+
"stop"
|
|
22475
|
+
]),
|
|
22476
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
22477
|
+
wildcardMatcherMeansAll: true
|
|
22478
|
+
}
|
|
22479
|
+
};
|
|
22480
|
+
/**
|
|
22481
|
+
* IBM Bob lifecycle hooks.
|
|
22482
|
+
*
|
|
22483
|
+
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
22484
|
+
* `<project>/.bob/settings.json` (project scope) and
|
|
22485
|
+
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
22486
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
22487
|
+
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
22488
|
+
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
22489
|
+
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
22490
|
+
*
|
|
22491
|
+
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
22492
|
+
*/
|
|
22493
|
+
var BobHooks = class extends SettingsJsonHooks {
|
|
22494
|
+
static getSpec() {
|
|
22495
|
+
return BOB_SPEC;
|
|
22496
|
+
}
|
|
22497
|
+
static getSettablePaths({ global = false } = {}) {
|
|
22498
|
+
return {
|
|
22499
|
+
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
22500
|
+
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
22501
|
+
};
|
|
22502
|
+
}
|
|
22503
|
+
};
|
|
22504
|
+
//#endregion
|
|
22505
|
+
//#region src/features/hooks/claudecode-hooks.ts
|
|
22506
|
+
const CLAUDE_SPEC = {
|
|
22507
|
+
displayName: "Claude",
|
|
22508
|
+
overrideKey: "claudecode",
|
|
22509
|
+
converterConfig: {
|
|
22510
|
+
supportedEvents: CLAUDE_HOOK_EVENTS,
|
|
22511
|
+
canonicalToToolEventNames: CANONICAL_TO_CLAUDE_EVENT_NAMES,
|
|
22512
|
+
toolToCanonicalEventNames: CLAUDE_TO_CANONICAL_EVENT_NAMES,
|
|
22513
|
+
projectDirVar: "$CLAUDE_PROJECT_DIR",
|
|
22514
|
+
prefixDotRelativeCommandsOnly: true,
|
|
22515
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22516
|
+
"worktreeCreate",
|
|
22517
|
+
"worktreeRemove",
|
|
22518
|
+
"messageDisplay",
|
|
22519
|
+
"postToolBatch",
|
|
22520
|
+
"taskCreated",
|
|
22521
|
+
"taskCompleted",
|
|
22522
|
+
"teammateIdle",
|
|
22523
|
+
"cwdChanged",
|
|
22524
|
+
"beforeSubmitPrompt",
|
|
22525
|
+
"stop"
|
|
22526
|
+
]),
|
|
22527
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22528
|
+
"command",
|
|
22529
|
+
"prompt",
|
|
22530
|
+
"http",
|
|
22531
|
+
"mcp_tool",
|
|
22532
|
+
"agent"
|
|
22533
|
+
]),
|
|
22534
|
+
emitsPromptModel: true,
|
|
22535
|
+
stringPassthroughFields: [
|
|
22536
|
+
{
|
|
22537
|
+
canonical: "if",
|
|
22538
|
+
tool: "if"
|
|
22539
|
+
},
|
|
22540
|
+
{
|
|
22541
|
+
canonical: "statusMessage",
|
|
22542
|
+
tool: "statusMessage"
|
|
22543
|
+
},
|
|
22544
|
+
{
|
|
22545
|
+
canonical: "shell",
|
|
22546
|
+
tool: "shell",
|
|
22547
|
+
commandOnly: true
|
|
22548
|
+
}
|
|
22549
|
+
],
|
|
22550
|
+
booleanPassthroughFields: [
|
|
22551
|
+
{
|
|
22552
|
+
canonical: "once",
|
|
22553
|
+
tool: "once"
|
|
22554
|
+
},
|
|
22555
|
+
{
|
|
22556
|
+
canonical: "async",
|
|
22557
|
+
tool: "async",
|
|
22558
|
+
commandOnly: true
|
|
22559
|
+
},
|
|
22560
|
+
{
|
|
22561
|
+
canonical: "asyncRewake",
|
|
22562
|
+
tool: "asyncRewake",
|
|
22563
|
+
commandOnly: true
|
|
22564
|
+
},
|
|
22565
|
+
{
|
|
22566
|
+
canonical: "continueOnBlock",
|
|
22567
|
+
tool: "continueOnBlock"
|
|
22568
|
+
}
|
|
22569
|
+
],
|
|
22570
|
+
arrayPassthroughFields: [{
|
|
22571
|
+
canonical: "args",
|
|
22572
|
+
tool: "args",
|
|
22573
|
+
commandOnly: true
|
|
22574
|
+
}]
|
|
22575
|
+
}
|
|
22576
|
+
};
|
|
22577
|
+
var ClaudecodeHooks = class extends SettingsJsonHooks {
|
|
22578
|
+
static getSpec() {
|
|
22579
|
+
return CLAUDE_SPEC;
|
|
22580
|
+
}
|
|
22581
|
+
static getSettablePaths(_options = {}) {
|
|
22582
|
+
return {
|
|
22583
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
22584
|
+
relativeFilePath: CLAUDECODE_SETTINGS_FILE_NAME
|
|
22585
|
+
};
|
|
22586
|
+
}
|
|
22587
|
+
/**
|
|
22588
|
+
* Every Claude-shaped settings file rulesync writes for Claude Code — the
|
|
22589
|
+
* project and user `settings.json` and the plugin bundle's `hooks.json` — is
|
|
22590
|
+
* declared under the one Claude key in `SHARED_CONFIG_OWNERSHIP`.
|
|
22591
|
+
*/
|
|
22592
|
+
static getSharedFileKey(_paths) {
|
|
22593
|
+
return CLAUDE_SETTINGS_SHARED_FILE_KEY;
|
|
22594
|
+
}
|
|
22595
|
+
static supportsPreserveUnowned() {
|
|
22596
|
+
return true;
|
|
22597
|
+
}
|
|
22598
|
+
};
|
|
22334
22599
|
//#endregion
|
|
22335
22600
|
//#region src/features/hooks/claudecode-plugin-hooks.ts
|
|
22336
22601
|
var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
@@ -22857,47 +23122,22 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
22857
23122
|
}
|
|
22858
23123
|
};
|
|
22859
23124
|
//#endregion
|
|
22860
|
-
//#region src/features/hooks/
|
|
22861
|
-
const
|
|
22862
|
-
supportedEvents:
|
|
22863
|
-
canonicalToToolEventNames:
|
|
22864
|
-
toolToCanonicalEventNames:
|
|
22865
|
-
projectDirVar: "$
|
|
23125
|
+
//#region src/features/hooks/commandcode-hooks.ts
|
|
23126
|
+
const COMMANDCODE_CONVERTER_CONFIG = {
|
|
23127
|
+
supportedEvents: COMMANDCODE_HOOK_EVENTS,
|
|
23128
|
+
canonicalToToolEventNames: CANONICAL_TO_COMMANDCODE_EVENT_NAMES,
|
|
23129
|
+
toolToCanonicalEventNames: COMMANDCODE_TO_CANONICAL_EVENT_NAMES,
|
|
23130
|
+
projectDirVar: "$COMMANDCODE_PROJECT_DIR",
|
|
22866
23131
|
prefixDotRelativeCommandsOnly: true,
|
|
22867
|
-
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22868
|
-
|
|
22869
|
-
"stop",
|
|
22870
|
-
"teammateIdle",
|
|
22871
|
-
"taskCompleted",
|
|
22872
|
-
"worktreeCreate",
|
|
22873
|
-
"worktreeRemove"
|
|
22874
|
-
]),
|
|
22875
|
-
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22876
|
-
"command",
|
|
22877
|
-
"prompt",
|
|
22878
|
-
"http",
|
|
22879
|
-
"agent"
|
|
22880
|
-
]),
|
|
22881
|
-
emitsPromptModel: true,
|
|
22882
|
-
stringPassthroughFields: [{
|
|
22883
|
-
canonical: "statusMessage",
|
|
22884
|
-
tool: "statusMessage"
|
|
22885
|
-
}],
|
|
22886
|
-
booleanPassthroughFields: [{
|
|
22887
|
-
canonical: "once",
|
|
22888
|
-
tool: "once"
|
|
22889
|
-
}, {
|
|
22890
|
-
canonical: "async",
|
|
22891
|
-
tool: "async",
|
|
22892
|
-
commandOnly: true
|
|
22893
|
-
}]
|
|
23132
|
+
noMatcherEvents: /* @__PURE__ */ new Set(["stop", "sessionStart"]),
|
|
23133
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
22894
23134
|
};
|
|
22895
23135
|
/**
|
|
22896
|
-
* Single spelling of the settings
|
|
22897
|
-
*
|
|
22898
|
-
*
|
|
23136
|
+
* Single spelling of the settings codec/policy: fail closed on an unparseable
|
|
23137
|
+
* root rather than replacing the user's Command Code settings with generated
|
|
23138
|
+
* output.
|
|
22899
23139
|
*/
|
|
22900
|
-
function
|
|
23140
|
+
function parseCommandcodeSettings$1(fileContent, filePath) {
|
|
22901
23141
|
return parseSharedConfig({
|
|
22902
23142
|
format: "json",
|
|
22903
23143
|
fileContent,
|
|
@@ -22906,22 +23146,20 @@ function parseContinueSettings(fileContent, filePath) {
|
|
|
22906
23146
|
});
|
|
22907
23147
|
}
|
|
22908
23148
|
/**
|
|
22909
|
-
*
|
|
23149
|
+
* Command Code hooks.
|
|
22910
23150
|
*
|
|
22911
|
-
* Hooks live under the top-level `hooks` key of `<project>/.
|
|
22912
|
-
* (project scope) and `~/.
|
|
23151
|
+
* Hooks live under the top-level `hooks` key of `<project>/.commandcode/settings.json`
|
|
23152
|
+
* (project scope) and `~/.commandcode/settings.json` (user scope), in the
|
|
22913
23153
|
* Claude-Code shape: `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{
|
|
22914
|
-
* "type": "command"
|
|
22915
|
-
*
|
|
22916
|
-
* merges the `hooks` key into the existing file (see
|
|
22917
|
-
* instead of overwriting it.
|
|
22918
|
-
* `.continue/settings.local.json` and the Claude Code settings files; rulesync
|
|
22919
|
-
* writes only the committable project file and the user file.
|
|
23154
|
+
* "type": "command", "command": "...", "timeout"?: <seconds> }] }] }`. Both
|
|
23155
|
+
* files also hold settings rulesync does not own (`permissions`, `defaultMode`,
|
|
23156
|
+
* ...), so generation merges the `hooks` key into the existing file (see
|
|
23157
|
+
* `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
|
|
22920
23158
|
*
|
|
22921
|
-
* @see https://
|
|
22922
|
-
* @see https://
|
|
23159
|
+
* @see https://commandcode.ai/docs/hooks
|
|
23160
|
+
* @see https://commandcode.ai/docs/settings
|
|
22923
23161
|
*/
|
|
22924
|
-
var
|
|
23162
|
+
var CommandcodeHooks = class CommandcodeHooks extends ToolHooks {
|
|
22925
23163
|
constructor(params) {
|
|
22926
23164
|
super({
|
|
22927
23165
|
...params,
|
|
@@ -22933,14 +23171,14 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22933
23171
|
}
|
|
22934
23172
|
static getSettablePaths(_options = {}) {
|
|
22935
23173
|
return {
|
|
22936
|
-
relativeDirPath:
|
|
22937
|
-
relativeFilePath:
|
|
23174
|
+
relativeDirPath: COMMANDCODE_DIR,
|
|
23175
|
+
relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
|
|
22938
23176
|
};
|
|
22939
23177
|
}
|
|
22940
23178
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22941
|
-
const paths =
|
|
23179
|
+
const paths = CommandcodeHooks.getSettablePaths({ global });
|
|
22942
23180
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22943
|
-
return new
|
|
23181
|
+
return new CommandcodeHooks({
|
|
22944
23182
|
outputRoot,
|
|
22945
23183
|
relativeDirPath: paths.relativeDirPath,
|
|
22946
23184
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22949,14 +23187,14 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22949
23187
|
});
|
|
22950
23188
|
}
|
|
22951
23189
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
22952
|
-
const paths =
|
|
23190
|
+
const paths = CommandcodeHooks.getSettablePaths({ global });
|
|
22953
23191
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22954
23192
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22955
23193
|
const config = rulesyncHooks.getJson();
|
|
22956
23194
|
const hooks = canonicalToToolHooks({
|
|
22957
23195
|
config,
|
|
22958
|
-
toolOverrideHooks: config.
|
|
22959
|
-
converterConfig:
|
|
23196
|
+
toolOverrideHooks: config.commandcode?.hooks,
|
|
23197
|
+
converterConfig: COMMANDCODE_CONVERTER_CONFIG,
|
|
22960
23198
|
logger
|
|
22961
23199
|
});
|
|
22962
23200
|
const fileContent = applySharedConfigPatch({
|
|
@@ -22967,7 +23205,7 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22967
23205
|
filePath,
|
|
22968
23206
|
logger
|
|
22969
23207
|
});
|
|
22970
|
-
return new
|
|
23208
|
+
return new CommandcodeHooks({
|
|
22971
23209
|
outputRoot,
|
|
22972
23210
|
relativeDirPath: paths.relativeDirPath,
|
|
22973
23211
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22979,18 +23217,18 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22979
23217
|
const configPath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
22980
23218
|
let settings;
|
|
22981
23219
|
try {
|
|
22982
|
-
settings =
|
|
23220
|
+
settings = parseCommandcodeSettings$1(this.getFileContent(), configPath);
|
|
22983
23221
|
} catch (error) {
|
|
22984
|
-
throw new Error(`Failed to parse
|
|
23222
|
+
throw new Error(`Failed to parse Command Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
22985
23223
|
}
|
|
22986
23224
|
const hooks = toolHooksToCanonical({
|
|
22987
23225
|
logger,
|
|
22988
23226
|
hooks: settings.hooks,
|
|
22989
|
-
converterConfig:
|
|
23227
|
+
converterConfig: COMMANDCODE_CONVERTER_CONFIG
|
|
22990
23228
|
});
|
|
22991
23229
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
22992
23230
|
hooks,
|
|
22993
|
-
overrideKey: "
|
|
23231
|
+
overrideKey: "commandcode"
|
|
22994
23232
|
}), null, 2) });
|
|
22995
23233
|
}
|
|
22996
23234
|
validate() {
|
|
@@ -23000,7 +23238,7 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
23000
23238
|
};
|
|
23001
23239
|
}
|
|
23002
23240
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23003
|
-
return new
|
|
23241
|
+
return new CommandcodeHooks({
|
|
23004
23242
|
outputRoot,
|
|
23005
23243
|
relativeDirPath,
|
|
23006
23244
|
relativeFilePath,
|
|
@@ -23010,6 +23248,73 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
23010
23248
|
}
|
|
23011
23249
|
};
|
|
23012
23250
|
//#endregion
|
|
23251
|
+
//#region src/features/hooks/continue-hooks.ts
|
|
23252
|
+
const CONTINUE_SPEC = {
|
|
23253
|
+
displayName: "Continue",
|
|
23254
|
+
overrideKey: "continue",
|
|
23255
|
+
converterConfig: {
|
|
23256
|
+
supportedEvents: CONTINUE_HOOK_EVENTS,
|
|
23257
|
+
canonicalToToolEventNames: CANONICAL_TO_CONTINUE_EVENT_NAMES,
|
|
23258
|
+
toolToCanonicalEventNames: CONTINUE_TO_CANONICAL_EVENT_NAMES,
|
|
23259
|
+
projectDirVar: "$CONTINUE_PROJECT_DIR",
|
|
23260
|
+
prefixDotRelativeCommandsOnly: true,
|
|
23261
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
23262
|
+
"beforeSubmitPrompt",
|
|
23263
|
+
"stop",
|
|
23264
|
+
"teammateIdle",
|
|
23265
|
+
"taskCompleted",
|
|
23266
|
+
"worktreeCreate",
|
|
23267
|
+
"worktreeRemove"
|
|
23268
|
+
]),
|
|
23269
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
23270
|
+
"command",
|
|
23271
|
+
"prompt",
|
|
23272
|
+
"http",
|
|
23273
|
+
"agent"
|
|
23274
|
+
]),
|
|
23275
|
+
emitsPromptModel: true,
|
|
23276
|
+
stringPassthroughFields: [{
|
|
23277
|
+
canonical: "statusMessage",
|
|
23278
|
+
tool: "statusMessage"
|
|
23279
|
+
}],
|
|
23280
|
+
booleanPassthroughFields: [{
|
|
23281
|
+
canonical: "once",
|
|
23282
|
+
tool: "once"
|
|
23283
|
+
}, {
|
|
23284
|
+
canonical: "async",
|
|
23285
|
+
tool: "async",
|
|
23286
|
+
commandOnly: true
|
|
23287
|
+
}]
|
|
23288
|
+
}
|
|
23289
|
+
};
|
|
23290
|
+
/**
|
|
23291
|
+
* Continue CLI hooks.
|
|
23292
|
+
*
|
|
23293
|
+
* Hooks live under the top-level `hooks` key of `<project>/.continue/settings.json`
|
|
23294
|
+
* (project scope) and `~/.continue/settings.json` (user scope), in the
|
|
23295
|
+
* Claude-Code shape: `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{
|
|
23296
|
+
* "type": "command" | "http" | "prompt" | "agent", ..., "timeout"?: <seconds>
|
|
23297
|
+
* }] }] }`. Both files also hold settings rulesync does not own, so generation
|
|
23298
|
+
* merges the `hooks` key into the existing file (see `SHARED_CONFIG_OWNERSHIP`)
|
|
23299
|
+
* instead of overwriting it. The CLI additionally reads
|
|
23300
|
+
* `.continue/settings.local.json` and the Claude Code settings files; rulesync
|
|
23301
|
+
* writes only the committable project file and the user file.
|
|
23302
|
+
*
|
|
23303
|
+
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/hookConfig.ts
|
|
23304
|
+
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/types.ts
|
|
23305
|
+
*/
|
|
23306
|
+
var ContinueHooks = class extends SettingsJsonHooks {
|
|
23307
|
+
static getSpec() {
|
|
23308
|
+
return CONTINUE_SPEC;
|
|
23309
|
+
}
|
|
23310
|
+
static getSettablePaths(_options = {}) {
|
|
23311
|
+
return {
|
|
23312
|
+
relativeDirPath: CONTINUE_DIR,
|
|
23313
|
+
relativeFilePath: CONTINUE_SETTINGS_FILE_NAME
|
|
23314
|
+
};
|
|
23315
|
+
}
|
|
23316
|
+
};
|
|
23317
|
+
//#endregion
|
|
23013
23318
|
//#region src/features/hooks/copilot-hooks.ts
|
|
23014
23319
|
/**
|
|
23015
23320
|
* Copilot hook entry as stored in .github/hooks/copilot-hooks.json.
|
|
@@ -23598,33 +23903,24 @@ const CORTEXCODE_SKILLS_DIR_PATH = join(CORTEXCODE_DIR, "skills");
|
|
|
23598
23903
|
const CORTEXCODE_GLOBAL_SKILLS_DIR_PATH = join(CORTEXCODE_GLOBAL_DIR_PATH, "skills");
|
|
23599
23904
|
//#endregion
|
|
23600
23905
|
//#region src/features/hooks/cortexcode-hooks.ts
|
|
23601
|
-
const
|
|
23602
|
-
|
|
23603
|
-
|
|
23604
|
-
|
|
23605
|
-
|
|
23606
|
-
|
|
23607
|
-
|
|
23608
|
-
|
|
23609
|
-
|
|
23610
|
-
|
|
23611
|
-
|
|
23612
|
-
|
|
23906
|
+
const CORTEXCODE_SPEC = {
|
|
23907
|
+
displayName: "Cortex Code",
|
|
23908
|
+
overrideKey: "cortexcode",
|
|
23909
|
+
converterConfig: {
|
|
23910
|
+
supportedEvents: CORTEXCODE_HOOK_EVENTS,
|
|
23911
|
+
canonicalToToolEventNames: CANONICAL_TO_CORTEXCODE_EVENT_NAMES,
|
|
23912
|
+
toolToCanonicalEventNames: CORTEXCODE_TO_CANONICAL_EVENT_NAMES,
|
|
23913
|
+
projectDirVar: "$CORTEX_PROJECT_DIR",
|
|
23914
|
+
prefixDotRelativeCommandsOnly: true,
|
|
23915
|
+
noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
|
|
23916
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
23917
|
+
booleanPassthroughFields: [{
|
|
23918
|
+
canonical: "enabled",
|
|
23919
|
+
tool: "enabled"
|
|
23920
|
+
}]
|
|
23921
|
+
}
|
|
23613
23922
|
};
|
|
23614
23923
|
/**
|
|
23615
|
-
* Single spelling of the settings/hooks codec/policy: fail closed on an
|
|
23616
|
-
* unparseable root rather than replacing the user's Cortex Code settings with
|
|
23617
|
-
* generated output.
|
|
23618
|
-
*/
|
|
23619
|
-
function parseCortexcodeSettings(fileContent, filePath) {
|
|
23620
|
-
return parseSharedConfig({
|
|
23621
|
-
format: "json",
|
|
23622
|
-
fileContent,
|
|
23623
|
-
filePath,
|
|
23624
|
-
invalidRootPolicy: "error"
|
|
23625
|
-
});
|
|
23626
|
-
}
|
|
23627
|
-
/**
|
|
23628
23924
|
* Snowflake Cortex Code hooks.
|
|
23629
23925
|
*
|
|
23630
23926
|
* Hooks live under the top-level `hooks` key of `<project>/.cortex/settings.json`
|
|
@@ -23633,20 +23929,16 @@ function parseCortexcodeSettings(fileContent, filePath) {
|
|
|
23633
23929
|
* "<regex>", "hooks": [{ "type": "command" | "prompt", ..., "timeout"?:
|
|
23634
23930
|
* <seconds>, "enabled"?: <boolean> }] }] }`. The project file also holds
|
|
23635
23931
|
* settings rulesync does not own, so generation merges the `hooks` key into
|
|
23636
|
-
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it
|
|
23932
|
+
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it, and
|
|
23933
|
+
* neither file is removed wholesale (hooks.json sits in the CLI-owned
|
|
23934
|
+
* `~/.snowflake/cortex/` tree).
|
|
23637
23935
|
*
|
|
23638
23936
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/extensibility
|
|
23639
23937
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/settings
|
|
23640
23938
|
*/
|
|
23641
|
-
var CortexcodeHooks = class
|
|
23642
|
-
|
|
23643
|
-
|
|
23644
|
-
...params,
|
|
23645
|
-
fileContent: params.fileContent ?? "{}"
|
|
23646
|
-
});
|
|
23647
|
-
}
|
|
23648
|
-
isDeletable() {
|
|
23649
|
-
return false;
|
|
23939
|
+
var CortexcodeHooks = class extends SettingsJsonHooks {
|
|
23940
|
+
static getSpec() {
|
|
23941
|
+
return CORTEXCODE_SPEC;
|
|
23650
23942
|
}
|
|
23651
23943
|
static getSettablePaths({ global = false } = {}) {
|
|
23652
23944
|
return global ? {
|
|
@@ -23657,60 +23949,338 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23657
23949
|
relativeFilePath: CORTEXCODE_SETTINGS_FILE_NAME
|
|
23658
23950
|
};
|
|
23659
23951
|
}
|
|
23952
|
+
};
|
|
23953
|
+
//#endregion
|
|
23954
|
+
//#region src/constants/crush-paths.ts
|
|
23955
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
23956
|
+
const CRUSH_GLOBAL_DIR = join(".config", "crush");
|
|
23957
|
+
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
23958
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
23959
|
+
const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
|
|
23960
|
+
const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
|
|
23961
|
+
const CRUSH_CONFIG_FILE_NAME = "crush.json";
|
|
23962
|
+
const CRUSH_HIDDEN_CONFIG_FILE_NAME = ".crush.json";
|
|
23963
|
+
const CRUSH_PERMISSIONS_KEY = "permissions";
|
|
23964
|
+
const CRUSH_ALLOWED_TOOLS_KEY = "allowed_tools";
|
|
23965
|
+
const CRUSH_OPTIONS_KEY = "options";
|
|
23966
|
+
const CRUSH_DISABLED_TOOLS_KEY = "disabled_tools";
|
|
23967
|
+
const CRUSH_HOOKS_KEY = "hooks";
|
|
23968
|
+
//#endregion
|
|
23969
|
+
//#region src/features/crush-config.ts
|
|
23970
|
+
/**
|
|
23971
|
+
* Where Crush's JSON config lives for a scope, before the project-scope twin
|
|
23972
|
+
* is resolved: `<project>/crush.json` or `~/.config/crush/crush.json`. This is
|
|
23973
|
+
* also the key every adapter reports through `getSettablePaths`, so the
|
|
23974
|
+
* shared-config ownership declaration, the gitignore derivation and the
|
|
23975
|
+
* shared-write ordering all see one file per scope.
|
|
23976
|
+
*/
|
|
23977
|
+
function getCrushConfigSettablePaths({ global = false } = {}) {
|
|
23978
|
+
return {
|
|
23979
|
+
relativeDirPath: global ? CRUSH_GLOBAL_DIR : ".",
|
|
23980
|
+
relativeFilePath: CRUSH_CONFIG_FILE_NAME
|
|
23981
|
+
};
|
|
23982
|
+
}
|
|
23983
|
+
/**
|
|
23984
|
+
* Resolves which Crush config file an adapter should read and write.
|
|
23985
|
+
*
|
|
23986
|
+
* At project scope Crush discovers both `.crush.json` and `crush.json` in the
|
|
23987
|
+
* working directory and merges every file it finds with `.crush.json` on top
|
|
23988
|
+
* (objects merge recursively, lists are concatenated, scalars are overridden).
|
|
23989
|
+
* Neither file therefore hides the other: an entry left in `crush.json` stays
|
|
23990
|
+
* in effect next to whatever `.crush.json` says. An existing `.crush.json` is
|
|
23991
|
+
* preferred because the user chose that spelling; otherwise `crush.json` is
|
|
23992
|
+
* used (and created when neither exists). The global scope has a single
|
|
23993
|
+
* spelling.
|
|
23994
|
+
*
|
|
23995
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
23996
|
+
*/
|
|
23997
|
+
async function resolveCrushConfigFile({ outputRoot, global = false }) {
|
|
23998
|
+
const paths = getCrushConfigSettablePaths({ global });
|
|
23999
|
+
const configDir = join(outputRoot, paths.relativeDirPath);
|
|
24000
|
+
const filePath = join(configDir, paths.relativeFilePath);
|
|
24001
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
24002
|
+
if (!global) {
|
|
24003
|
+
const hiddenPath = join(configDir, CRUSH_HIDDEN_CONFIG_FILE_NAME);
|
|
24004
|
+
const hiddenContent = await readFileContentOrNull(hiddenPath);
|
|
24005
|
+
if (hiddenContent !== null) return {
|
|
24006
|
+
relativeDirPath: paths.relativeDirPath,
|
|
24007
|
+
relativeFilePath: CRUSH_HIDDEN_CONFIG_FILE_NAME,
|
|
24008
|
+
filePath: hiddenPath,
|
|
24009
|
+
fileContent: hiddenContent,
|
|
24010
|
+
...fileContent === null ? {} : { twin: {
|
|
24011
|
+
filePath,
|
|
24012
|
+
fileContent
|
|
24013
|
+
} }
|
|
24014
|
+
};
|
|
24015
|
+
}
|
|
24016
|
+
return {
|
|
24017
|
+
relativeDirPath: paths.relativeDirPath,
|
|
24018
|
+
relativeFilePath: paths.relativeFilePath,
|
|
24019
|
+
filePath,
|
|
24020
|
+
fileContent
|
|
24021
|
+
};
|
|
24022
|
+
}
|
|
24023
|
+
/**
|
|
24024
|
+
* Single spelling of the crush.json codec/policy, matching the
|
|
24025
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
24026
|
+
* unparseable root rather than replacing the user's Crush config with
|
|
24027
|
+
* generated output.
|
|
24028
|
+
*/
|
|
24029
|
+
function parseCrushConfig(fileContent, filePath) {
|
|
24030
|
+
return parseSharedConfig({
|
|
24031
|
+
format: "json",
|
|
24032
|
+
fileContent,
|
|
24033
|
+
filePath,
|
|
24034
|
+
invalidRootPolicy: "error"
|
|
24035
|
+
});
|
|
24036
|
+
}
|
|
24037
|
+
/**
|
|
24038
|
+
* Combine two Crush config documents the way Crush itself does
|
|
24039
|
+
* (`github.com/qjebbs/go-jsons`): objects merge recursively, arrays are
|
|
24040
|
+
* concatenated with `base` first, and any other value from `override` wins.
|
|
24041
|
+
*/
|
|
24042
|
+
function mergeCrushConfigs({ base, override }) {
|
|
24043
|
+
const merged = { ...base };
|
|
24044
|
+
for (const [key, value] of Object.entries(override)) {
|
|
24045
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
24046
|
+
const current = merged[key];
|
|
24047
|
+
if (isRecord$1(current) && isRecord$1(value)) merged[key] = mergeCrushConfigs({
|
|
24048
|
+
base: current,
|
|
24049
|
+
override: value
|
|
24050
|
+
});
|
|
24051
|
+
else if (Array.isArray(current) && Array.isArray(value)) merged[key] = [...current, ...value];
|
|
24052
|
+
else merged[key] = value;
|
|
24053
|
+
}
|
|
24054
|
+
return merged;
|
|
24055
|
+
}
|
|
24056
|
+
/**
|
|
24057
|
+
* The content an import should read for a resolved location: the chosen file
|
|
24058
|
+
* merged over its twin when both exist, so a server, hook or tool entry that
|
|
24059
|
+
* only the lower-priority `crush.json` declares is imported too — Crush sees
|
|
24060
|
+
* it, so rulesync should as well.
|
|
24061
|
+
*/
|
|
24062
|
+
function crushConfigImportContent(location) {
|
|
24063
|
+
const own = location.fileContent ?? "{}";
|
|
24064
|
+
if (location.twin === void 0) return own;
|
|
24065
|
+
const merged = mergeCrushConfigs({
|
|
24066
|
+
base: parseCrushConfig(location.twin.fileContent, location.twin.filePath),
|
|
24067
|
+
override: parseCrushConfig(own, location.filePath)
|
|
24068
|
+
});
|
|
24069
|
+
return JSON.stringify(merged, null, 2);
|
|
24070
|
+
}
|
|
24071
|
+
function lookupPath(root, path) {
|
|
24072
|
+
let current = root;
|
|
24073
|
+
for (const segment of path) {
|
|
24074
|
+
if (!isRecord$1(current) || !Object.hasOwn(current, segment)) return;
|
|
24075
|
+
current = current[segment];
|
|
24076
|
+
}
|
|
24077
|
+
return current;
|
|
24078
|
+
}
|
|
24079
|
+
function isNonEmptyValue(value) {
|
|
24080
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
24081
|
+
if (isRecord$1(value)) return Object.keys(value).length > 0;
|
|
24082
|
+
return value !== void 0;
|
|
24083
|
+
}
|
|
24084
|
+
/**
|
|
24085
|
+
* Warn when the twin Crush merges beneath the written file still carries a
|
|
24086
|
+
* value at one of the paths this feature owns. Rulesync only rewrites the
|
|
24087
|
+
* chosen file, and because Crush concatenates lists and merges objects across
|
|
24088
|
+
* the pair, such an entry — typically one an earlier generate wrote to
|
|
24089
|
+
* `crush.json` before the user added a `.crush.json` — stays in effect until
|
|
24090
|
+
* removed by hand, and cannot be retracted from `.rulesync/`.
|
|
24091
|
+
*/
|
|
24092
|
+
function warnCrushTwinLeftovers({ location, ownedPaths, logger }) {
|
|
24093
|
+
if (location.twin === void 0) return;
|
|
24094
|
+
let twin;
|
|
24095
|
+
try {
|
|
24096
|
+
twin = parseCrushConfig(location.twin.fileContent, location.twin.filePath);
|
|
24097
|
+
} catch {
|
|
24098
|
+
return;
|
|
24099
|
+
}
|
|
24100
|
+
const leftovers = ownedPaths.filter((path) => isNonEmptyValue(lookupPath(twin, path))).map((path) => `"${path.join(".")}"`);
|
|
24101
|
+
if (leftovers.length === 0) return;
|
|
24102
|
+
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.`);
|
|
24103
|
+
}
|
|
24104
|
+
//#endregion
|
|
24105
|
+
//#region src/features/hooks/crush-hooks.ts
|
|
24106
|
+
const SUPPORTED_CRUSH_EVENTS = new Set(CRUSH_HOOK_EVENTS);
|
|
24107
|
+
/**
|
|
24108
|
+
* Crush matches an event key case-insensitively and ignores underscores
|
|
24109
|
+
* (`PreToolUse`, `pre_tool_use` and `PRE_TOOL_USE` all work), so an import
|
|
24110
|
+
* looks the canonical name up by that normalized spelling.
|
|
24111
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
24112
|
+
*/
|
|
24113
|
+
const NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CRUSH_TO_CANONICAL_EVENT_NAMES).map(([crushEvent, canonical]) => [normalizeCrushEventName(crushEvent), canonical]));
|
|
24114
|
+
function normalizeCrushEventName(event) {
|
|
24115
|
+
return event.replaceAll("_", "").toLowerCase();
|
|
24116
|
+
}
|
|
24117
|
+
/**
|
|
24118
|
+
* Build the `hooks` block of `crush.json` from a canonical hooks config.
|
|
24119
|
+
* Crush keys a flat array of `{name, matcher, command, timeout}` entries by
|
|
24120
|
+
* event name; `matcher` is a regex tested against the (lower-case) Crush tool
|
|
24121
|
+
* name and `timeout` is in seconds. Only `type: "command"` canonical hooks are
|
|
24122
|
+
* emitted, since a Crush hook is a shell command. A shared canonical event
|
|
24123
|
+
* Crush does not fire is skipped (the HooksProcessor reports it), while an
|
|
24124
|
+
* event under the `crush.hooks` override — such as one an import filed there
|
|
24125
|
+
* — is written verbatim so it round-trips.
|
|
24126
|
+
*/
|
|
24127
|
+
function canonicalToCrushHooks({ config, toolOverride, logger }) {
|
|
24128
|
+
const sharedHooks = {};
|
|
24129
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_CRUSH_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
24130
|
+
const effective = {
|
|
24131
|
+
...sharedHooks,
|
|
24132
|
+
...toolOverride
|
|
24133
|
+
};
|
|
24134
|
+
const hooks = {};
|
|
24135
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
24136
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
24137
|
+
const crushEvent = lookupOwn({
|
|
24138
|
+
record: CANONICAL_TO_CRUSH_EVENT_NAMES,
|
|
24139
|
+
key: event
|
|
24140
|
+
}) ?? event;
|
|
24141
|
+
const entries = [];
|
|
24142
|
+
for (const def of defs) {
|
|
24143
|
+
const entry = canonicalDefToCrushEntry({
|
|
24144
|
+
def,
|
|
24145
|
+
event,
|
|
24146
|
+
logger
|
|
24147
|
+
});
|
|
24148
|
+
if (entry !== null) entries.push(entry);
|
|
24149
|
+
}
|
|
24150
|
+
if (entries.length > 0) hooks[crushEvent] = entries;
|
|
24151
|
+
}
|
|
24152
|
+
return hooks;
|
|
24153
|
+
}
|
|
24154
|
+
/** Convert one canonical hook definition to a Crush entry, or null to skip. */
|
|
24155
|
+
function canonicalDefToCrushEntry({ def, event, logger }) {
|
|
24156
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
24157
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
24158
|
+
logger?.warn(`Crush hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Crush would discard, so it was skipped.`);
|
|
24159
|
+
return null;
|
|
24160
|
+
}
|
|
24161
|
+
const entry = { command: def.command };
|
|
24162
|
+
if (typeof def.name === "string" && def.name !== "") entry.name = def.name;
|
|
24163
|
+
if (typeof def.matcher === "string" && def.matcher !== "" && def.matcher !== "*") entry.matcher = def.matcher;
|
|
24164
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
24165
|
+
return entry;
|
|
24166
|
+
}
|
|
24167
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
24168
|
+
function crushEntryToCanonicalDef(raw) {
|
|
24169
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string") return null;
|
|
24170
|
+
const def = {
|
|
24171
|
+
type: "command",
|
|
24172
|
+
command: raw.command
|
|
24173
|
+
};
|
|
24174
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
24175
|
+
if (typeof raw.matcher === "string" && raw.matcher !== "") def.matcher = raw.matcher;
|
|
24176
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
24177
|
+
return def;
|
|
24178
|
+
}
|
|
24179
|
+
/**
|
|
24180
|
+
* Reverse {@link canonicalToCrushHooks}: parse the `hooks` block back into a
|
|
24181
|
+
* canonical event → definition[] record. An event Crush does not document is
|
|
24182
|
+
* carried under its own name so `buildImportedHooksConfig` files it under the
|
|
24183
|
+
* `crush.hooks` override.
|
|
24184
|
+
*/
|
|
24185
|
+
function crushHooksToCanonical(hooksBlock) {
|
|
24186
|
+
const canonical = {};
|
|
24187
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
24188
|
+
for (const [crushEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
24189
|
+
if (isPrototypePollutionKey(crushEvent) || !Array.isArray(rawEntries)) continue;
|
|
24190
|
+
const canonicalEvent = lookupOwn({
|
|
24191
|
+
record: NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES,
|
|
24192
|
+
key: normalizeCrushEventName(crushEvent)
|
|
24193
|
+
}) ?? crushEvent;
|
|
24194
|
+
const defs = rawEntries.map((raw) => crushEntryToCanonicalDef(raw)).filter((def) => def !== null);
|
|
24195
|
+
if (defs.length === 0) continue;
|
|
24196
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
24197
|
+
record: canonical,
|
|
24198
|
+
key: canonicalEvent
|
|
24199
|
+
}) ?? [], ...defs];
|
|
24200
|
+
}
|
|
24201
|
+
return canonical;
|
|
24202
|
+
}
|
|
24203
|
+
/**
|
|
24204
|
+
* Crush hooks.
|
|
24205
|
+
*
|
|
24206
|
+
* Crush reads hooks from the `hooks` key of its JSON config —
|
|
24207
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
24208
|
+
* lists concatenated) at project scope and `~/.config/crush/crush.json` at
|
|
24209
|
+
* user scope — as
|
|
24210
|
+
* `hooks.<Event>: [{name?, matcher?, command, timeout?}]`. Only `PreToolUse`
|
|
24211
|
+
* fires today. The `hooks` key is owned outright; every other top-level key
|
|
24212
|
+
* of the file is preserved and the file is never deleted.
|
|
24213
|
+
*
|
|
24214
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
24215
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
24216
|
+
*/
|
|
24217
|
+
var CrushHooks = class CrushHooks extends ToolHooks {
|
|
24218
|
+
json;
|
|
24219
|
+
constructor(params) {
|
|
24220
|
+
super(params);
|
|
24221
|
+
this.json = parseCrushConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
24222
|
+
}
|
|
24223
|
+
getJson() {
|
|
24224
|
+
return this.json;
|
|
24225
|
+
}
|
|
24226
|
+
isDeletable() {
|
|
24227
|
+
return false;
|
|
24228
|
+
}
|
|
24229
|
+
static getSettablePaths({ global = false } = {}) {
|
|
24230
|
+
return getCrushConfigSettablePaths({ global });
|
|
24231
|
+
}
|
|
23660
24232
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
23661
|
-
const
|
|
23662
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
23663
|
-
return new CortexcodeHooks({
|
|
24233
|
+
const location = await resolveCrushConfigFile({
|
|
23664
24234
|
outputRoot,
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
|
|
24235
|
+
global
|
|
24236
|
+
});
|
|
24237
|
+
return new CrushHooks({
|
|
24238
|
+
outputRoot,
|
|
24239
|
+
relativeDirPath: location.relativeDirPath,
|
|
24240
|
+
relativeFilePath: location.relativeFilePath,
|
|
24241
|
+
fileContent: crushConfigImportContent(location),
|
|
24242
|
+
validate,
|
|
24243
|
+
global
|
|
23669
24244
|
});
|
|
23670
24245
|
}
|
|
23671
24246
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
23672
|
-
const
|
|
23673
|
-
|
|
23674
|
-
|
|
23675
|
-
|
|
23676
|
-
const
|
|
23677
|
-
|
|
23678
|
-
|
|
23679
|
-
|
|
24247
|
+
const location = await resolveCrushConfigFile({
|
|
24248
|
+
outputRoot,
|
|
24249
|
+
global
|
|
24250
|
+
});
|
|
24251
|
+
const existingContent = location.fileContent ?? "";
|
|
24252
|
+
warnCrushTwinLeftovers({
|
|
24253
|
+
location,
|
|
24254
|
+
ownedPaths: [[CRUSH_HOOKS_KEY]],
|
|
23680
24255
|
logger
|
|
23681
24256
|
});
|
|
23682
|
-
const
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
patch: { hooks },
|
|
23687
|
-
filePath,
|
|
24257
|
+
const config = rulesyncHooks.getJson();
|
|
24258
|
+
const hooks = canonicalToCrushHooks({
|
|
24259
|
+
config,
|
|
24260
|
+
toolOverride: config.crush?.hooks,
|
|
23688
24261
|
logger
|
|
23689
24262
|
});
|
|
23690
|
-
return new
|
|
24263
|
+
return new CrushHooks({
|
|
23691
24264
|
outputRoot,
|
|
23692
|
-
relativeDirPath:
|
|
23693
|
-
relativeFilePath:
|
|
23694
|
-
fileContent
|
|
23695
|
-
|
|
24265
|
+
relativeDirPath: location.relativeDirPath,
|
|
24266
|
+
relativeFilePath: location.relativeFilePath,
|
|
24267
|
+
fileContent: applySharedConfigPatch({
|
|
24268
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
24269
|
+
feature: "hooks",
|
|
24270
|
+
existingContent,
|
|
24271
|
+
patch: { [CRUSH_HOOKS_KEY]: hooks },
|
|
24272
|
+
filePath: location.filePath,
|
|
24273
|
+
logger
|
|
24274
|
+
}),
|
|
24275
|
+
validate,
|
|
24276
|
+
global
|
|
23696
24277
|
});
|
|
23697
24278
|
}
|
|
23698
|
-
toRulesyncHooks(
|
|
23699
|
-
const
|
|
23700
|
-
let settings;
|
|
23701
|
-
try {
|
|
23702
|
-
settings = parseCortexcodeSettings(this.getFileContent(), configPath);
|
|
23703
|
-
} catch (error) {
|
|
23704
|
-
throw new Error(`Failed to parse Cortex Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
23705
|
-
}
|
|
23706
|
-
const hooks = toolHooksToCanonical({
|
|
23707
|
-
logger,
|
|
23708
|
-
hooks: settings.hooks,
|
|
23709
|
-
converterConfig: CORTEXCODE_CONVERTER_CONFIG
|
|
23710
|
-
});
|
|
24279
|
+
toRulesyncHooks() {
|
|
24280
|
+
const hooks = crushHooksToCanonical(this.json[CRUSH_HOOKS_KEY]);
|
|
23711
24281
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
23712
24282
|
hooks,
|
|
23713
|
-
overrideKey: "
|
|
24283
|
+
overrideKey: "crush"
|
|
23714
24284
|
}), null, 2) });
|
|
23715
24285
|
}
|
|
23716
24286
|
validate() {
|
|
@@ -23719,13 +24289,14 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23719
24289
|
error: null
|
|
23720
24290
|
};
|
|
23721
24291
|
}
|
|
23722
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23723
|
-
return new
|
|
24292
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
24293
|
+
return new CrushHooks({
|
|
23724
24294
|
outputRoot,
|
|
23725
24295
|
relativeDirPath,
|
|
23726
24296
|
relativeFilePath,
|
|
23727
|
-
fileContent: JSON.stringify({
|
|
23728
|
-
validate: false
|
|
24297
|
+
fileContent: JSON.stringify({ [CRUSH_HOOKS_KEY]: {} }, null, 2),
|
|
24298
|
+
validate: false,
|
|
24299
|
+
global
|
|
23729
24300
|
});
|
|
23730
24301
|
}
|
|
23731
24302
|
};
|
|
@@ -26959,19 +27530,22 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
|
26959
27530
|
//#endregion
|
|
26960
27531
|
//#region src/features/hooks/tabnine-hooks.ts
|
|
26961
27532
|
/**
|
|
26962
|
-
* Environment block safe to hand Tabnine for a hook.
|
|
26963
|
-
*
|
|
26964
|
-
*
|
|
26965
|
-
*
|
|
26966
|
-
*
|
|
27533
|
+
* Environment block safe to hand Tabnine for a hook. The shared converter
|
|
27534
|
+
* refuses a whole `env` map holding one bad entry; Tabnine's shape is authored
|
|
27535
|
+
* outside it, so the same per-entry rule (`isSafeEnvEntry`: a key holding `=`,
|
|
27536
|
+
* a control character or nothing at all names a different variable than it
|
|
27537
|
+
* appears to) is applied entry by entry and the bad ones are dropped in both
|
|
27538
|
+
* directions, with a warning on export, where the value came from an authored
|
|
27539
|
+
* `.rulesync/hooks.*`.
|
|
26967
27540
|
*/
|
|
26968
27541
|
function sanitizeEnv({ env, warn }) {
|
|
26969
27542
|
if (env === null || env === void 0 || typeof env !== "object" || Array.isArray(env)) return;
|
|
26970
27543
|
const result = {};
|
|
26971
27544
|
for (const [key, value] of Object.entries(env)) {
|
|
26972
|
-
|
|
26973
|
-
|
|
26974
|
-
|
|
27545
|
+
if (!isSafeEnvEntry({
|
|
27546
|
+
key,
|
|
27547
|
+
value
|
|
27548
|
+
})) {
|
|
26975
27549
|
warn?.(`Tabnine CLI hook env entry ${JSON.stringify(key)} is not a safe KEY=VALUE pair; skipping it.`);
|
|
26976
27550
|
continue;
|
|
26977
27551
|
}
|
|
@@ -27072,7 +27646,7 @@ const TabnineMatcherEntrySchema = z.looseObject({
|
|
|
27072
27646
|
});
|
|
27073
27647
|
function tabnineMatcherEntryToCanonical(entry) {
|
|
27074
27648
|
const sequential = entry.sequential === true;
|
|
27075
|
-
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
27649
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && entry.matcher !== ".*" ? entry.matcher : void 0;
|
|
27076
27650
|
const defs = [];
|
|
27077
27651
|
for (const hook of entry.hooks ?? []) {
|
|
27078
27652
|
if (hook.type !== "command" || typeof hook.command !== "string" || hook.command === "") continue;
|
|
@@ -27793,6 +28367,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
27793
28367
|
supportedHookTypes: ["command"],
|
|
27794
28368
|
supportsMatcher: true
|
|
27795
28369
|
}],
|
|
28370
|
+
["commandcode", {
|
|
28371
|
+
class: CommandcodeHooks,
|
|
28372
|
+
meta: {
|
|
28373
|
+
supportsProject: true,
|
|
28374
|
+
supportsGlobal: true,
|
|
28375
|
+
supportsImport: true
|
|
28376
|
+
},
|
|
28377
|
+
supportedEvents: COMMANDCODE_HOOK_EVENTS,
|
|
28378
|
+
supportedHookTypes: ["command"],
|
|
28379
|
+
supportsMatcher: true,
|
|
28380
|
+
matcherEvents: ["preToolUse", "postToolUse"]
|
|
28381
|
+
}],
|
|
27796
28382
|
["copilot", {
|
|
27797
28383
|
class: CopilotHooks,
|
|
27798
28384
|
meta: {
|
|
@@ -28114,6 +28700,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28114
28700
|
supportedHookTypes: ["command", "http"],
|
|
28115
28701
|
supportsMatcher: true
|
|
28116
28702
|
}],
|
|
28703
|
+
["crush", {
|
|
28704
|
+
class: CrushHooks,
|
|
28705
|
+
meta: {
|
|
28706
|
+
supportsProject: true,
|
|
28707
|
+
supportsGlobal: true,
|
|
28708
|
+
supportsImport: true
|
|
28709
|
+
},
|
|
28710
|
+
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
28711
|
+
supportedHookTypes: ["command"],
|
|
28712
|
+
supportsMatcher: true,
|
|
28713
|
+
passthroughOverrideEvents: true
|
|
28714
|
+
}],
|
|
28117
28715
|
["zcode", {
|
|
28118
28716
|
class: ZcodeHooks,
|
|
28119
28717
|
meta: {
|
|
@@ -28795,14 +29393,6 @@ var ContinueIgnore = class ContinueIgnore extends ToolIgnore {
|
|
|
28795
29393
|
}
|
|
28796
29394
|
};
|
|
28797
29395
|
//#endregion
|
|
28798
|
-
//#region src/constants/crush-paths.ts
|
|
28799
|
-
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
28800
|
-
const CRUSH_GLOBAL_DIR = join(".config", "crush");
|
|
28801
|
-
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
28802
|
-
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
28803
|
-
const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
|
|
28804
|
-
const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
|
|
28805
|
-
//#endregion
|
|
28806
29396
|
//#region src/features/ignore/crush-ignore.ts
|
|
28807
29397
|
/**
|
|
28808
29398
|
* Ignore generator for Crush.
|
|
@@ -30804,9 +31394,9 @@ function asBobRemoteType(stated, url) {
|
|
|
30804
31394
|
* SSE server carries a bare `url`. The canonical `transport` alias and the
|
|
30805
31395
|
* Claude-style `httpUrl` alias are folded into `type`/`url`; `env`, `cwd`,
|
|
30806
31396
|
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through, as Bob
|
|
30807
|
-
* documents all of them
|
|
30808
|
-
*
|
|
30809
|
-
* the HTTP requests).
|
|
31397
|
+
* documents all of them, with prototype-pollution keys dropped at every
|
|
31398
|
+
* nesting level (Bob spreads `env` and `headers` into the process environment
|
|
31399
|
+
* and the HTTP requests).
|
|
30810
31400
|
*
|
|
30811
31401
|
* Bob Shell documents the same file with an `httpURL` key instead of
|
|
30812
31402
|
* `type` + `url` for streamable HTTP. rulesync writes the IDE spelling (the two
|
|
@@ -30874,7 +31464,7 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30874
31464
|
}
|
|
30875
31465
|
for (const [key, value] of Object.entries(rest)) {
|
|
30876
31466
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30877
|
-
converted[key] = (
|
|
31467
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30878
31468
|
}
|
|
30879
31469
|
result[serverName] = converted;
|
|
30880
31470
|
}
|
|
@@ -30885,7 +31475,9 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30885
31475
|
* (`type: "streamable-http"` + `url`) is already canonical and passes through;
|
|
30886
31476
|
* the Bob Shell spelling `httpURL` becomes `url` with `type: "http"`; a bare
|
|
30887
31477
|
* `url` is an SSE server in Bob, so it gains `type: "sse"` to keep that reading
|
|
30888
|
-
* on the next generate.
|
|
31478
|
+
* on the next generate. Every other field passes through with its
|
|
31479
|
+
* prototype-pollution keys dropped at every nesting level, mirroring the
|
|
31480
|
+
* generate side's handling of `env` / `headers`.
|
|
30889
31481
|
*/
|
|
30890
31482
|
function convertFromBobFormat(mcpServers) {
|
|
30891
31483
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -30900,7 +31492,7 @@ function convertFromBobFormat(mcpServers) {
|
|
|
30900
31492
|
if (typeof value === "string") httpURL = value;
|
|
30901
31493
|
continue;
|
|
30902
31494
|
}
|
|
30903
|
-
converted[key] = value;
|
|
31495
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30904
31496
|
}
|
|
30905
31497
|
if (httpURL !== void 0) {
|
|
30906
31498
|
converted.url = httpURL;
|
|
@@ -31644,6 +32236,254 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
31644
32236
|
}
|
|
31645
32237
|
};
|
|
31646
32238
|
//#endregion
|
|
32239
|
+
//#region src/features/mcp/commandcode-mcp.ts
|
|
32240
|
+
/**
|
|
32241
|
+
* Parse a Command Code MCP file. The project file is the `.mcp.json` other
|
|
32242
|
+
* agents (Claude Code among them) share, so a hand-written file is read as
|
|
32243
|
+
* JSONC to tolerate comments and trailing commas; malformed content or a
|
|
32244
|
+
* non-object root (`null`, an array, a scalar) fails closed rather than being
|
|
32245
|
+
* spread into the regenerated file.
|
|
32246
|
+
*/
|
|
32247
|
+
function parseCommandcodeMcpConfig({ fileContent, relativePath }) {
|
|
32248
|
+
let parsed;
|
|
32249
|
+
try {
|
|
32250
|
+
parsed = parseJsonc(fileContent);
|
|
32251
|
+
} catch (error) {
|
|
32252
|
+
throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
32253
|
+
}
|
|
32254
|
+
if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse Command Code MCP config at ${relativePath}: expected a JSON object at the root`);
|
|
32255
|
+
return parsed;
|
|
32256
|
+
}
|
|
32257
|
+
/**
|
|
32258
|
+
* The remote transport Command Code reads a server as. Its loader accepts
|
|
32259
|
+
* `http` (streamable HTTP) and `sse` for a `url` server; a bare `url` defaults
|
|
32260
|
+
* to `http`, and the canonical `streamable-http` spelling is folded into it.
|
|
32261
|
+
* A `ws(s)://` URL or any other stated transport has no Command Code
|
|
32262
|
+
* equivalent, so `undefined` tells the caller to skip the server.
|
|
32263
|
+
* @see https://commandcode.ai/docs/mcp
|
|
32264
|
+
*/
|
|
32265
|
+
function asCommandcodeRemoteTransport(stated, url) {
|
|
32266
|
+
if (stated === "sse") return "sse";
|
|
32267
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
32268
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
32269
|
+
}
|
|
32270
|
+
/**
|
|
32271
|
+
* Convert the canonical server map to the shape Command Code's config schema
|
|
32272
|
+
* documents (used for the global `~/.commandcode/mcp.json` only — the project
|
|
32273
|
+
* `.mcp.json` is shared with Claude Code and written pass-through, see the
|
|
32274
|
+
* class doc): a remote server is `{ transport, url, headers?, env?, oauth? }`
|
|
32275
|
+
* and a stdio server is `{ transport: "stdio", command, args?, env? }`, each
|
|
32276
|
+
* with an optional `enabled` flag. The documented spelling is `transport`
|
|
32277
|
+
* (`type` is only accepted as an alias on read), so that is what is written,
|
|
32278
|
+
* and only the documented keys are emitted — `timeout`, rulesync-only fields
|
|
32279
|
+
* and anything else is left out. The canonical `disabled: true` becomes
|
|
32280
|
+
* Command Code's native `enabled: false` (the rulesync-source-only `enabled`
|
|
32281
|
+
* filter never reaches this function: `getMcpServers()` drops such a server).
|
|
32282
|
+
*
|
|
32283
|
+
* A server Command Code drops at load time — no transport at all, a remote
|
|
32284
|
+
* transport without a URL, a WebSocket URL, or a stdio entry without a
|
|
32285
|
+
* command — is skipped with a warning rather than written in a form the tool
|
|
32286
|
+
* would silently ignore.
|
|
32287
|
+
* @see https://commandcode.ai/docs/mcp
|
|
32288
|
+
*/
|
|
32289
|
+
function convertToCommandcodeFormat(mcpServers, logger) {
|
|
32290
|
+
const result = {};
|
|
32291
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32292
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32293
|
+
if (declaresNoTransport(serverConfig)) {
|
|
32294
|
+
warnAndSkipMcpServer({
|
|
32295
|
+
toolName: "Command Code",
|
|
32296
|
+
serverName,
|
|
32297
|
+
reason: "no transport",
|
|
32298
|
+
logger
|
|
32299
|
+
});
|
|
32300
|
+
continue;
|
|
32301
|
+
}
|
|
32302
|
+
const converted = isRemoteMcpServer(serverConfig) ? convertRemoteServer$1({
|
|
32303
|
+
serverName,
|
|
32304
|
+
serverConfig,
|
|
32305
|
+
logger
|
|
32306
|
+
}) : convertStdioServer$1({
|
|
32307
|
+
serverName,
|
|
32308
|
+
serverConfig,
|
|
32309
|
+
logger
|
|
32310
|
+
});
|
|
32311
|
+
if (converted === void 0) continue;
|
|
32312
|
+
if (serverConfig.disabled === true) converted.enabled = false;
|
|
32313
|
+
if (isRecord$1(serverConfig.env)) converted.env = omitPrototypePollutionKeys(serverConfig.env);
|
|
32314
|
+
result[serverName] = converted;
|
|
32315
|
+
}
|
|
32316
|
+
return result;
|
|
32317
|
+
}
|
|
32318
|
+
function convertRemoteServer$1({ serverName, serverConfig, logger }) {
|
|
32319
|
+
const url = resolveRemoteMcpUrl(serverConfig);
|
|
32320
|
+
if (!url) {
|
|
32321
|
+
warnAndSkipMcpServer({
|
|
32322
|
+
toolName: "Command Code",
|
|
32323
|
+
serverName,
|
|
32324
|
+
reason: "a remote transport without a url",
|
|
32325
|
+
logger
|
|
32326
|
+
});
|
|
32327
|
+
return;
|
|
32328
|
+
}
|
|
32329
|
+
const stated = serverConfig.type ?? serverConfig.transport;
|
|
32330
|
+
const transport = asCommandcodeRemoteTransport(typeof stated === "string" ? stated : void 0, url);
|
|
32331
|
+
if (transport === void 0) {
|
|
32332
|
+
warnAndSkipMcpServer({
|
|
32333
|
+
toolName: "Command Code",
|
|
32334
|
+
serverName,
|
|
32335
|
+
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)`,
|
|
32336
|
+
logger
|
|
32337
|
+
});
|
|
32338
|
+
return;
|
|
32339
|
+
}
|
|
32340
|
+
const converted = {
|
|
32341
|
+
transport,
|
|
32342
|
+
url
|
|
32343
|
+
};
|
|
32344
|
+
if (isRecord$1(serverConfig.headers)) converted.headers = omitPrototypePollutionKeys(serverConfig.headers);
|
|
32345
|
+
if (isRecord$1(serverConfig.oauth)) converted.oauth = omitPrototypePollutionKeys(serverConfig.oauth);
|
|
32346
|
+
return converted;
|
|
32347
|
+
}
|
|
32348
|
+
function convertStdioServer$1({ serverName, serverConfig, logger }) {
|
|
32349
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
32350
|
+
if (!command) {
|
|
32351
|
+
warnAndSkipMcpServer({
|
|
32352
|
+
toolName: "Command Code",
|
|
32353
|
+
serverName,
|
|
32354
|
+
reason: "a stdio transport without a command",
|
|
32355
|
+
logger
|
|
32356
|
+
});
|
|
32357
|
+
return;
|
|
32358
|
+
}
|
|
32359
|
+
const converted = {
|
|
32360
|
+
transport: "stdio",
|
|
32361
|
+
command
|
|
32362
|
+
};
|
|
32363
|
+
if (args.length > 0) converted.args = args;
|
|
32364
|
+
return converted;
|
|
32365
|
+
}
|
|
32366
|
+
/**
|
|
32367
|
+
* Convert Command Code's server map back to the canonical shape. Both
|
|
32368
|
+
* spellings Command Code accepts (`transport` and its `type` alias, with
|
|
32369
|
+
* `command`/`url`) are already canonical, so entries pass through with only
|
|
32370
|
+
* prototype-pollution keys dropped; the native `enabled: false` maps to the
|
|
32371
|
+
* canonical `disabled: true` (a bare `enabled` would otherwise be read back
|
|
32372
|
+
* as rulesync's own generation filter and silently drop the server from every
|
|
32373
|
+
* other target).
|
|
32374
|
+
*/
|
|
32375
|
+
function convertFromCommandcodeFormat(mcpServers) {
|
|
32376
|
+
if (!isMcpServers(mcpServers)) return {};
|
|
32377
|
+
const result = {};
|
|
32378
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32379
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32380
|
+
const { enabled, ...rest } = omitPrototypePollutionKeys(serverConfig);
|
|
32381
|
+
result[serverName] = enabled === false ? {
|
|
32382
|
+
...rest,
|
|
32383
|
+
disabled: true
|
|
32384
|
+
} : rest;
|
|
32385
|
+
}
|
|
32386
|
+
return result;
|
|
32387
|
+
}
|
|
32388
|
+
/**
|
|
32389
|
+
* Command Code MCP configuration.
|
|
32390
|
+
*
|
|
32391
|
+
* Command Code reads `.mcp.json` at the project root (project scope, meant to
|
|
32392
|
+
* be committed) and `~/.commandcode/mcp.json` (user scope), both in the
|
|
32393
|
+
* `{ "mcpServers": { ... } }` shape the docs show; the per-machine local
|
|
32394
|
+
* scope under `~/.commandcode/projects/` is left to the tool. Top-level
|
|
32395
|
+
* sibling keys of an existing file are kept. The global file is not deleted
|
|
32396
|
+
* by `--delete` (it lives outside the project), while the project file is
|
|
32397
|
+
* rulesync's own and is.
|
|
32398
|
+
*
|
|
32399
|
+
* The project `.mcp.json` is the very file the `claudecode` target writes, so
|
|
32400
|
+
* at project scope the servers are written in the same pass-through shape
|
|
32401
|
+
* `ClaudecodeMcp` uses: whichever of the two targets generates last leaves
|
|
32402
|
+
* byte-identical content, and Command Code reads that shape natively (`type`
|
|
32403
|
+
* is an alias of `transport`, a bare `url` infers `http`, and unknown keys
|
|
32404
|
+
* are ignored). Only the global file — Command Code's own — gets the
|
|
32405
|
+
* `transport` / `enabled: false` rewrite of `convertToCommandcodeFormat`.
|
|
32406
|
+
*
|
|
32407
|
+
* @see https://commandcode.ai/docs/mcp
|
|
32408
|
+
*/
|
|
32409
|
+
var CommandcodeMcp = class CommandcodeMcp extends ToolMcp {
|
|
32410
|
+
json;
|
|
32411
|
+
constructor(params) {
|
|
32412
|
+
super(params);
|
|
32413
|
+
this.json = this.fileContent === void 0 ? {} : parseCommandcodeMcpConfig({
|
|
32414
|
+
fileContent: this.fileContent,
|
|
32415
|
+
relativePath: join(this.relativeDirPath, this.relativeFilePath)
|
|
32416
|
+
});
|
|
32417
|
+
}
|
|
32418
|
+
getJson() {
|
|
32419
|
+
return this.json;
|
|
32420
|
+
}
|
|
32421
|
+
isDeletable() {
|
|
32422
|
+
return !this.global;
|
|
32423
|
+
}
|
|
32424
|
+
static getSettablePaths({ global = false } = {}) {
|
|
32425
|
+
return global ? {
|
|
32426
|
+
relativeDirPath: COMMANDCODE_DIR,
|
|
32427
|
+
relativeFilePath: COMMANDCODE_GLOBAL_MCP_FILE_NAME
|
|
32428
|
+
} : {
|
|
32429
|
+
relativeDirPath: ".",
|
|
32430
|
+
relativeFilePath: COMMANDCODE_PROJECT_MCP_FILE_NAME
|
|
32431
|
+
};
|
|
32432
|
+
}
|
|
32433
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32434
|
+
const paths = this.getSettablePaths({ global });
|
|
32435
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32436
|
+
return new CommandcodeMcp({
|
|
32437
|
+
outputRoot,
|
|
32438
|
+
relativeDirPath: paths.relativeDirPath,
|
|
32439
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32440
|
+
fileContent,
|
|
32441
|
+
validate,
|
|
32442
|
+
global
|
|
32443
|
+
});
|
|
32444
|
+
}
|
|
32445
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
32446
|
+
const paths = this.getSettablePaths({ global });
|
|
32447
|
+
const json = parseCommandcodeMcpConfig({
|
|
32448
|
+
fileContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32449
|
+
relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
|
|
32450
|
+
});
|
|
32451
|
+
const mcpServers = global ? convertToCommandcodeFormat(rulesyncMcp.getMcpServers(), logger) : rulesyncMcp.getMcpServers();
|
|
32452
|
+
const commandcodeConfig = {
|
|
32453
|
+
...json,
|
|
32454
|
+
mcpServers
|
|
32455
|
+
};
|
|
32456
|
+
return new CommandcodeMcp({
|
|
32457
|
+
outputRoot,
|
|
32458
|
+
relativeDirPath: paths.relativeDirPath,
|
|
32459
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32460
|
+
fileContent: JSON.stringify(commandcodeConfig, null, 2),
|
|
32461
|
+
validate,
|
|
32462
|
+
global
|
|
32463
|
+
});
|
|
32464
|
+
}
|
|
32465
|
+
toRulesyncMcp() {
|
|
32466
|
+
const mcpServers = convertFromCommandcodeFormat(this.json.mcpServers);
|
|
32467
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
32468
|
+
}
|
|
32469
|
+
validate() {
|
|
32470
|
+
return {
|
|
32471
|
+
success: true,
|
|
32472
|
+
error: null
|
|
32473
|
+
};
|
|
32474
|
+
}
|
|
32475
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
32476
|
+
return new CommandcodeMcp({
|
|
32477
|
+
outputRoot,
|
|
32478
|
+
relativeDirPath,
|
|
32479
|
+
relativeFilePath,
|
|
32480
|
+
fileContent: "{}",
|
|
32481
|
+
validate: false,
|
|
32482
|
+
global
|
|
32483
|
+
});
|
|
32484
|
+
}
|
|
32485
|
+
};
|
|
32486
|
+
//#endregion
|
|
31647
32487
|
//#region src/features/mcp/continue-mcp.ts
|
|
31648
32488
|
/**
|
|
31649
32489
|
* Parse a Continue MCP file. Continue reads the files under `mcpServers/` as
|
|
@@ -32306,7 +33146,7 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32306
33146
|
}
|
|
32307
33147
|
for (const [key, value] of Object.entries(rest)) {
|
|
32308
33148
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32309
|
-
converted[key] = (
|
|
33149
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
32310
33150
|
}
|
|
32311
33151
|
result[serverName] = converted;
|
|
32312
33152
|
}
|
|
@@ -32315,14 +33155,15 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32315
33155
|
/**
|
|
32316
33156
|
* Convert Cortex Code's server map back to the canonical shape. The
|
|
32317
33157
|
* documented spelling (`type` + `command`/`url`) is already canonical, so
|
|
32318
|
-
* entries pass through with only prototype-pollution keys dropped
|
|
33158
|
+
* entries pass through with only prototype-pollution keys dropped — at every
|
|
33159
|
+
* nesting level, mirroring the generate side.
|
|
32319
33160
|
*/
|
|
32320
33161
|
function convertFromCortexcodeFormat(mcpServers) {
|
|
32321
33162
|
if (!isMcpServers(mcpServers)) return {};
|
|
32322
33163
|
const result = {};
|
|
32323
33164
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32324
33165
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32325
|
-
result[serverName] =
|
|
33166
|
+
result[serverName] = omitPrototypePollutionKeysDeep(serverConfig);
|
|
32326
33167
|
}
|
|
32327
33168
|
return result;
|
|
32328
33169
|
}
|
|
@@ -32361,19 +33202,12 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32361
33202
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32362
33203
|
if (!global) throw new Error(CORTEXCODE_GLOBAL_ONLY_MESSAGE);
|
|
32363
33204
|
const paths = this.getSettablePaths({ global });
|
|
32364
|
-
const
|
|
32365
|
-
fileContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32366
|
-
relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
|
|
32367
|
-
});
|
|
32368
|
-
const newJson = {
|
|
32369
|
-
...json,
|
|
32370
|
-
mcpServers: json.mcpServers ?? {}
|
|
32371
|
-
};
|
|
33205
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32372
33206
|
return new CortexcodeMcp({
|
|
32373
33207
|
outputRoot,
|
|
32374
33208
|
relativeDirPath: paths.relativeDirPath,
|
|
32375
33209
|
relativeFilePath: paths.relativeFilePath,
|
|
32376
|
-
fileContent
|
|
33210
|
+
fileContent,
|
|
32377
33211
|
validate,
|
|
32378
33212
|
global
|
|
32379
33213
|
});
|
|
@@ -32421,6 +33255,319 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32421
33255
|
}
|
|
32422
33256
|
};
|
|
32423
33257
|
//#endregion
|
|
33258
|
+
//#region src/features/mcp/crush-mcp.ts
|
|
33259
|
+
/**
|
|
33260
|
+
* Crush-only MCP fields with no canonical counterpart, authored under a
|
|
33261
|
+
* `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
|
|
33262
|
+
* name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers and
|
|
33263
|
+
* `sessionless` marks a server that issues no `Mcp-Session-Id`.
|
|
33264
|
+
* `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
|
|
33265
|
+
* never reaches another tool's config; `fromRulesyncMcp` re-merges them from
|
|
33266
|
+
* the raw source JSON, honouring the raw Crush spelling as a fallback for an
|
|
33267
|
+
* entry copied straight out of a `crush.json` (the `experimental_environment`
|
|
33268
|
+
* precedent) — except `oauth`, whose canonical key is Claude Code's
|
|
33269
|
+
* `{ clientId }` object and which only `crushOauth` can set.
|
|
33270
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
33271
|
+
*/
|
|
33272
|
+
const CRUSH_ONLY_KEYS = [
|
|
33273
|
+
{
|
|
33274
|
+
canonical: "crushOauth",
|
|
33275
|
+
crush: "oauth",
|
|
33276
|
+
accepts: (v) => typeof v === "boolean",
|
|
33277
|
+
rawFallback: false
|
|
33278
|
+
},
|
|
33279
|
+
{
|
|
33280
|
+
canonical: "crushOauthClientId",
|
|
33281
|
+
crush: "oauth_client_id",
|
|
33282
|
+
accepts: (v) => typeof v === "string",
|
|
33283
|
+
rawFallback: true
|
|
33284
|
+
},
|
|
33285
|
+
{
|
|
33286
|
+
canonical: "crushOauthClientSecret",
|
|
33287
|
+
crush: "oauth_client_secret",
|
|
33288
|
+
accepts: (v) => typeof v === "string",
|
|
33289
|
+
rawFallback: true
|
|
33290
|
+
},
|
|
33291
|
+
{
|
|
33292
|
+
canonical: "crushOauthCallbackPort",
|
|
33293
|
+
crush: "oauth_callback_port",
|
|
33294
|
+
accepts: (v) => Number.isInteger(v),
|
|
33295
|
+
rawFallback: true
|
|
33296
|
+
},
|
|
33297
|
+
{
|
|
33298
|
+
canonical: "crushSessionless",
|
|
33299
|
+
crush: "sessionless",
|
|
33300
|
+
accepts: (v) => typeof v === "boolean",
|
|
33301
|
+
rawFallback: true
|
|
33302
|
+
}
|
|
33303
|
+
];
|
|
33304
|
+
/**
|
|
33305
|
+
* The Crush-only keys of one raw canonical server entry, keyed by their
|
|
33306
|
+
* canonical name, so `convertToCrushFormat` sees them after the shared map
|
|
33307
|
+
* stripped them.
|
|
33308
|
+
*/
|
|
33309
|
+
function readCrushOnlyKeys(rawServer) {
|
|
33310
|
+
const result = {};
|
|
33311
|
+
if (!isRecord$1(rawServer)) return result;
|
|
33312
|
+
for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
|
|
33313
|
+
const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
|
|
33314
|
+
if (value !== void 0) result[canonical] = value;
|
|
33315
|
+
}
|
|
33316
|
+
return result;
|
|
33317
|
+
}
|
|
33318
|
+
function copyCrushOnlyKeys({ from, to }) {
|
|
33319
|
+
for (const { canonical, crush, accepts } of CRUSH_ONLY_KEYS) if (accepts(from[canonical])) to[crush] = from[canonical];
|
|
33320
|
+
}
|
|
33321
|
+
/**
|
|
33322
|
+
* The remote transports Crush's `MCPType` enum accepts, spelled the way it
|
|
33323
|
+
* reads them. `streamable-http` is the canonical rulesync name for what Crush
|
|
33324
|
+
* calls `http`; a WebSocket server has no Crush transport and is left to the
|
|
33325
|
+
* caller to reject.
|
|
33326
|
+
*/
|
|
33327
|
+
function asCrushRemoteType(stated, url) {
|
|
33328
|
+
if (stated === "sse") return "sse";
|
|
33329
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
33330
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
33331
|
+
}
|
|
33332
|
+
/**
|
|
33333
|
+
* The transport half of a remote server (`type`, `url`, `headers`), or null
|
|
33334
|
+
* when Crush could not reach it and the server was skipped with a warning.
|
|
33335
|
+
*/
|
|
33336
|
+
function convertRemoteTransport({ name, config, logger }) {
|
|
33337
|
+
const url = resolveRemoteMcpUrl(config);
|
|
33338
|
+
if (!url) {
|
|
33339
|
+
warnAndSkipMcpServer({
|
|
33340
|
+
toolName: "Crush",
|
|
33341
|
+
serverName: name,
|
|
33342
|
+
reason: "a remote transport without a url",
|
|
33343
|
+
logger
|
|
33344
|
+
});
|
|
33345
|
+
return null;
|
|
33346
|
+
}
|
|
33347
|
+
const stated = config.type ?? config.transport;
|
|
33348
|
+
const type = asCrushRemoteType(stated, url);
|
|
33349
|
+
if (type === void 0) {
|
|
33350
|
+
warnAndSkipMcpServer({
|
|
33351
|
+
toolName: "Crush",
|
|
33352
|
+
serverName: name,
|
|
33353
|
+
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)`,
|
|
33354
|
+
logger
|
|
33355
|
+
});
|
|
33356
|
+
return null;
|
|
33357
|
+
}
|
|
33358
|
+
const converted = {
|
|
33359
|
+
type,
|
|
33360
|
+
url
|
|
33361
|
+
};
|
|
33362
|
+
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
33363
|
+
return converted;
|
|
33364
|
+
}
|
|
33365
|
+
/**
|
|
33366
|
+
* The transport half of a stdio server (`type`, `command`, `args`, `env`), or
|
|
33367
|
+
* null when it has no command and was skipped with a warning.
|
|
33368
|
+
*/
|
|
33369
|
+
function convertStdioTransport({ name, config, logger }) {
|
|
33370
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
33371
|
+
if (!command) {
|
|
33372
|
+
warnAndSkipMcpServer({
|
|
33373
|
+
toolName: "Crush",
|
|
33374
|
+
serverName: name,
|
|
33375
|
+
reason: "a stdio transport without a command",
|
|
33376
|
+
logger
|
|
33377
|
+
});
|
|
33378
|
+
return null;
|
|
33379
|
+
}
|
|
33380
|
+
const converted = {
|
|
33381
|
+
type: "stdio",
|
|
33382
|
+
command
|
|
33383
|
+
};
|
|
33384
|
+
if (args.length > 0) converted.args = args;
|
|
33385
|
+
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
33386
|
+
return converted;
|
|
33387
|
+
}
|
|
33388
|
+
/**
|
|
33389
|
+
* Convert canonical rulesync servers to the `mcp.<name>` shape of crush.json.
|
|
33390
|
+
* `type` is required by Crush's schema, so it is always written: stdio servers
|
|
33391
|
+
* carry `type: "stdio"` with `command`/`args`/`env`, remote servers `http` or
|
|
33392
|
+
* `sse` with `url` and optional `headers`. `disabled`, the per-server
|
|
33393
|
+
* `disabledTools`/`enabledTools` filters (`disabled_tools`/`enabled_tools`)
|
|
33394
|
+
* and `timeout` (which Crush reads in seconds) map onto their Crush fields.
|
|
33395
|
+
*
|
|
33396
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
33397
|
+
*/
|
|
33398
|
+
function convertToCrushFormat(mcpServers, logger) {
|
|
33399
|
+
const result = {};
|
|
33400
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
33401
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
33402
|
+
if (!isRecord$1(config)) continue;
|
|
33403
|
+
if (declaresNoTransport(config)) {
|
|
33404
|
+
warnAndSkipMcpServer({
|
|
33405
|
+
toolName: "Crush",
|
|
33406
|
+
serverName: name,
|
|
33407
|
+
reason: "no transport",
|
|
33408
|
+
logger
|
|
33409
|
+
});
|
|
33410
|
+
continue;
|
|
33411
|
+
}
|
|
33412
|
+
const converted = isRemoteMcpServer(config) ? convertRemoteTransport({
|
|
33413
|
+
name,
|
|
33414
|
+
config,
|
|
33415
|
+
logger
|
|
33416
|
+
}) : convertStdioTransport({
|
|
33417
|
+
name,
|
|
33418
|
+
config,
|
|
33419
|
+
logger
|
|
33420
|
+
});
|
|
33421
|
+
if (converted === null) continue;
|
|
33422
|
+
if (config.disabled === true) converted.disabled = true;
|
|
33423
|
+
if (config.disabledTools && config.disabledTools.length > 0) converted.disabled_tools = config.disabledTools;
|
|
33424
|
+
if (config.enabledTools && config.enabledTools.length > 0) converted.enabled_tools = config.enabledTools;
|
|
33425
|
+
if (typeof config.timeout === "number") converted.timeout = Math.ceil(config.timeout);
|
|
33426
|
+
copyCrushOnlyKeys({
|
|
33427
|
+
from: config,
|
|
33428
|
+
to: converted
|
|
33429
|
+
});
|
|
33430
|
+
result[name] = converted;
|
|
33431
|
+
}
|
|
33432
|
+
return result;
|
|
33433
|
+
}
|
|
33434
|
+
/**
|
|
33435
|
+
* Convert the `mcp.<name>` entries of crush.json back to canonical rulesync
|
|
33436
|
+
* servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
|
|
33437
|
+
* server with a `command` is stdio already); `http` and `sse` are kept as the
|
|
33438
|
+
* canonical transport names; `disabled_tools`/`enabled_tools` become the
|
|
33439
|
+
* canonical `disabledTools`/`enabledTools`; the Crush-only OAuth and
|
|
33440
|
+
* `sessionless` fields are lifted into their `crush*` authoring keys (a value
|
|
33441
|
+
* of the wrong type is dropped, as Crush itself would refuse it), and any
|
|
33442
|
+
* other key is kept verbatim so an import preserves what a hand-authored
|
|
33443
|
+
* entry declared.
|
|
33444
|
+
*/
|
|
33445
|
+
function convertFromCrushFormat(crushServers) {
|
|
33446
|
+
const result = {};
|
|
33447
|
+
for (const [name, config] of Object.entries(crushServers)) {
|
|
33448
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
33449
|
+
const converted = {};
|
|
33450
|
+
for (const [key, value] of Object.entries(config)) {
|
|
33451
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
33452
|
+
switch (key) {
|
|
33453
|
+
case "type":
|
|
33454
|
+
if (value === "http" || value === "sse") converted.type = value;
|
|
33455
|
+
break;
|
|
33456
|
+
case "disabled_tools":
|
|
33457
|
+
if (isStringArray$2(value)) converted.disabledTools = value;
|
|
33458
|
+
break;
|
|
33459
|
+
case "enabled_tools":
|
|
33460
|
+
if (isStringArray$2(value)) converted.enabledTools = value;
|
|
33461
|
+
break;
|
|
33462
|
+
case "oauth_token": break;
|
|
33463
|
+
default: {
|
|
33464
|
+
const crushOnly = CRUSH_ONLY_KEYS.find((entry) => entry.crush === key);
|
|
33465
|
+
if (crushOnly === void 0) converted[key] = value;
|
|
33466
|
+
else if (crushOnly.accepts(value)) converted[crushOnly.canonical] = value;
|
|
33467
|
+
}
|
|
33468
|
+
}
|
|
33469
|
+
}
|
|
33470
|
+
result[name] = converted;
|
|
33471
|
+
}
|
|
33472
|
+
return result;
|
|
33473
|
+
}
|
|
33474
|
+
/**
|
|
33475
|
+
* Crush MCP servers.
|
|
33476
|
+
*
|
|
33477
|
+
* Crush reads MCP servers from the `mcp` key of its JSON config:
|
|
33478
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
33479
|
+
* objects recursively) at project scope and `~/.config/crush/crush.json` at
|
|
33480
|
+
* user scope. The
|
|
33481
|
+
* `crushrc` Bash config that Crush now recommends compiles its `mcp add`
|
|
33482
|
+
* builtin into the same `mcp.<name>` entries and overrides the JSON key by
|
|
33483
|
+
* key, so a `crushrc` next to the generated file takes precedence. Every
|
|
33484
|
+
* other top-level key of the file is preserved; the file is never deleted.
|
|
33485
|
+
*
|
|
33486
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
33487
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
33488
|
+
*/
|
|
33489
|
+
var CrushMcp = class CrushMcp extends ToolMcp {
|
|
33490
|
+
json;
|
|
33491
|
+
constructor(params) {
|
|
33492
|
+
super(params);
|
|
33493
|
+
this.json = parseCrushConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
33494
|
+
}
|
|
33495
|
+
getJson() {
|
|
33496
|
+
return this.json;
|
|
33497
|
+
}
|
|
33498
|
+
isDeletable() {
|
|
33499
|
+
return false;
|
|
33500
|
+
}
|
|
33501
|
+
static getSettablePaths({ global = false } = {}) {
|
|
33502
|
+
return getCrushConfigSettablePaths({ global });
|
|
33503
|
+
}
|
|
33504
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
33505
|
+
const location = await resolveCrushConfigFile({
|
|
33506
|
+
outputRoot,
|
|
33507
|
+
global
|
|
33508
|
+
});
|
|
33509
|
+
return new CrushMcp({
|
|
33510
|
+
outputRoot,
|
|
33511
|
+
relativeDirPath: location.relativeDirPath,
|
|
33512
|
+
relativeFilePath: location.relativeFilePath,
|
|
33513
|
+
fileContent: crushConfigImportContent(location),
|
|
33514
|
+
validate,
|
|
33515
|
+
global
|
|
33516
|
+
});
|
|
33517
|
+
}
|
|
33518
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
33519
|
+
const location = await resolveCrushConfigFile({
|
|
33520
|
+
outputRoot,
|
|
33521
|
+
global
|
|
33522
|
+
});
|
|
33523
|
+
const existingContent = location.fileContent ?? "";
|
|
33524
|
+
warnCrushTwinLeftovers({
|
|
33525
|
+
location,
|
|
33526
|
+
ownedPaths: [["mcp"]],
|
|
33527
|
+
logger
|
|
33528
|
+
});
|
|
33529
|
+
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
|
|
33530
|
+
...serverConfig,
|
|
33531
|
+
...readCrushOnlyKeys(rulesyncMcp.getRawMcpServer(serverName))
|
|
33532
|
+
}])), logger);
|
|
33533
|
+
return new CrushMcp({
|
|
33534
|
+
outputRoot,
|
|
33535
|
+
relativeDirPath: location.relativeDirPath,
|
|
33536
|
+
relativeFilePath: location.relativeFilePath,
|
|
33537
|
+
fileContent: applySharedConfigPatch({
|
|
33538
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
33539
|
+
feature: "mcp",
|
|
33540
|
+
existingContent,
|
|
33541
|
+
patch: { ["mcp"]: converted },
|
|
33542
|
+
filePath: location.filePath,
|
|
33543
|
+
logger
|
|
33544
|
+
}),
|
|
33545
|
+
validate,
|
|
33546
|
+
global
|
|
33547
|
+
});
|
|
33548
|
+
}
|
|
33549
|
+
toRulesyncMcp() {
|
|
33550
|
+
const converted = convertFromCrushFormat(isRecord$1(this.json["mcp"]) ? this.json["mcp"] : {});
|
|
33551
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
33552
|
+
}
|
|
33553
|
+
validate() {
|
|
33554
|
+
return {
|
|
33555
|
+
success: true,
|
|
33556
|
+
error: null
|
|
33557
|
+
};
|
|
33558
|
+
}
|
|
33559
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
33560
|
+
return new CrushMcp({
|
|
33561
|
+
outputRoot,
|
|
33562
|
+
relativeDirPath,
|
|
33563
|
+
relativeFilePath,
|
|
33564
|
+
fileContent: JSON.stringify({ ["mcp"]: {} }, null, 2),
|
|
33565
|
+
validate: false,
|
|
33566
|
+
global
|
|
33567
|
+
});
|
|
33568
|
+
}
|
|
33569
|
+
};
|
|
33570
|
+
//#endregion
|
|
32424
33571
|
//#region src/features/mcp/mcp-env-var-format.ts
|
|
32425
33572
|
/**
|
|
32426
33573
|
* Canonical rulesync env var reference pattern: `${VAR}` (but not `${env:VAR}`,
|
|
@@ -36305,6 +37452,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
36305
37452
|
static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
|
|
36306
37453
|
const servers = rulesyncMcp.forTarget({
|
|
36307
37454
|
toolTarget: "rovodev",
|
|
37455
|
+
global,
|
|
36308
37456
|
logger
|
|
36309
37457
|
}).getMcpServers();
|
|
36310
37458
|
const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
|
|
@@ -36465,7 +37613,7 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36465
37613
|
}
|
|
36466
37614
|
for (const [key, value] of Object.entries(rest)) {
|
|
36467
37615
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36468
|
-
converted[key] = (
|
|
37616
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36469
37617
|
}
|
|
36470
37618
|
if (enabledTools !== void 0) converted.includeTools = enabledTools;
|
|
36471
37619
|
if (disabledTools !== void 0) converted.excludeTools = disabledTools;
|
|
@@ -36476,7 +37624,9 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36476
37624
|
/**
|
|
36477
37625
|
* Convert Tabnine's server map back to the canonical shape: `includeTools` /
|
|
36478
37626
|
* `excludeTools` become `enabledTools` / `disabledTools`; `type` (`stdio`,
|
|
36479
|
-
* `sse`, `http`) and every other documented field are already canonical
|
|
37627
|
+
* `sse`, `http`) and every other documented field are already canonical and
|
|
37628
|
+
* pass through with their prototype-pollution keys dropped at every nesting
|
|
37629
|
+
* level, mirroring the generate side.
|
|
36480
37630
|
*/
|
|
36481
37631
|
function convertFromTabnineFormat(mcpServers) {
|
|
36482
37632
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -36488,7 +37638,7 @@ function convertFromTabnineFormat(mcpServers) {
|
|
|
36488
37638
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36489
37639
|
if (key === "includeTools") converted.enabledTools = value;
|
|
36490
37640
|
else if (key === "excludeTools") converted.disabledTools = value;
|
|
36491
|
-
else converted[key] = value;
|
|
37641
|
+
else converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36492
37642
|
}
|
|
36493
37643
|
result[serverName] = converted;
|
|
36494
37644
|
}
|
|
@@ -37600,6 +38750,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37600
38750
|
supportsDisabledTools: true
|
|
37601
38751
|
}
|
|
37602
38752
|
}],
|
|
38753
|
+
["commandcode", {
|
|
38754
|
+
class: CommandcodeMcp,
|
|
38755
|
+
meta: {
|
|
38756
|
+
supportsProject: true,
|
|
38757
|
+
supportsGlobal: true,
|
|
38758
|
+
supportsEnabledTools: false,
|
|
38759
|
+
supportsDisabledTools: false
|
|
38760
|
+
}
|
|
38761
|
+
}],
|
|
37603
38762
|
["continue", {
|
|
37604
38763
|
class: ContinueMcp,
|
|
37605
38764
|
meta: {
|
|
@@ -37636,6 +38795,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37636
38795
|
supportsDisabledTools: false
|
|
37637
38796
|
}
|
|
37638
38797
|
}],
|
|
38798
|
+
["crush", {
|
|
38799
|
+
class: CrushMcp,
|
|
38800
|
+
meta: {
|
|
38801
|
+
supportsProject: true,
|
|
38802
|
+
supportsGlobal: true,
|
|
38803
|
+
supportsEnabledTools: true,
|
|
38804
|
+
supportsDisabledTools: true
|
|
38805
|
+
}
|
|
38806
|
+
}],
|
|
37639
38807
|
["cursor", {
|
|
37640
38808
|
class: CursorMcp,
|
|
37641
38809
|
meta: {
|
|
@@ -37984,6 +39152,7 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
37984
39152
|
const toolMcps = await Promise.all([rulesyncMcp].map(async (mcp) => {
|
|
37985
39153
|
const targetedRulesyncMcp = mcp.forTarget({
|
|
37986
39154
|
toolTarget: this.toolTarget,
|
|
39155
|
+
global: this.global,
|
|
37987
39156
|
logger: this.logger
|
|
37988
39157
|
});
|
|
37989
39158
|
const fieldsToStrip = [];
|
|
@@ -39067,7 +40236,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
39067
40236
|
function toAntigravityCliToolName(canonical) {
|
|
39068
40237
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
39069
40238
|
}
|
|
39070
|
-
function toCanonicalToolName$
|
|
40239
|
+
function toCanonicalToolName$8(cliName) {
|
|
39071
40240
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
39072
40241
|
}
|
|
39073
40242
|
/**
|
|
@@ -39283,7 +40452,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
39283
40452
|
const processEntries = (entries, action) => {
|
|
39284
40453
|
for (const entry of entries) {
|
|
39285
40454
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
39286
|
-
const canonical = toCanonicalToolName$
|
|
40455
|
+
const canonical = toCanonicalToolName$8(toolName);
|
|
39287
40456
|
if (!permission[canonical]) permission[canonical] = {};
|
|
39288
40457
|
permission[canonical][pattern] = action;
|
|
39289
40458
|
}
|
|
@@ -39540,11 +40709,34 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
|
|
|
39540
40709
|
websearch: "web-search"
|
|
39541
40710
|
};
|
|
39542
40711
|
const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
40712
|
+
const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
|
|
40713
|
+
terminal: "launch-process",
|
|
40714
|
+
read: "view",
|
|
40715
|
+
edit: "str-replace-editor",
|
|
40716
|
+
write: "save-file"
|
|
40717
|
+
};
|
|
39543
40718
|
function toAugmentToolName(canonical) {
|
|
39544
|
-
return
|
|
40719
|
+
return lookupOwn({
|
|
40720
|
+
record: CANONICAL_TO_AUGMENT_TOOL_NAMES,
|
|
40721
|
+
key: canonical
|
|
40722
|
+
}) ?? canonical;
|
|
39545
40723
|
}
|
|
39546
|
-
|
|
39547
|
-
|
|
40724
|
+
/**
|
|
40725
|
+
* Resolve an existing entry's `toolName` to the legacy spelling rulesync emits, so that the
|
|
40726
|
+
* current alias and the legacy name are treated as the same managed tool.
|
|
40727
|
+
*/
|
|
40728
|
+
function toLegacyAugmentToolName(augmentName) {
|
|
40729
|
+
return lookupOwn({
|
|
40730
|
+
record: CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES,
|
|
40731
|
+
key: augmentName
|
|
40732
|
+
}) ?? augmentName;
|
|
40733
|
+
}
|
|
40734
|
+
function toCanonicalToolName$7(augmentName) {
|
|
40735
|
+
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
40736
|
+
return lookupOwn({
|
|
40737
|
+
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
40738
|
+
key: legacyName
|
|
40739
|
+
}) ?? legacyName;
|
|
39548
40740
|
}
|
|
39549
40741
|
function actionToAugmentType(action) {
|
|
39550
40742
|
switch (action) {
|
|
@@ -39760,10 +40952,11 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
39760
40952
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
39761
40953
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
39762
40954
|
if (entry.toolName === "*") return false;
|
|
39763
|
-
|
|
40955
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40956
|
+
if (!MANAGED_AUGMENT_TOOL_NAMES.has(legacyToolName)) return true;
|
|
39764
40957
|
if (entry.permission.type === "deny") {
|
|
39765
|
-
const
|
|
39766
|
-
return !generatedKeys.has(
|
|
40958
|
+
const suffix = `|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
40959
|
+
return !generatedKeys.has(`${entry.toolName}${suffix}`) && !generatedKeys.has(`${legacyToolName}${suffix}`);
|
|
39767
40960
|
}
|
|
39768
40961
|
return false;
|
|
39769
40962
|
});
|
|
@@ -39990,14 +41183,15 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
39990
41183
|
if (isSpecialEntry(entry)) continue;
|
|
39991
41184
|
const type = entry.permission.type;
|
|
39992
41185
|
if (!isBasicAugmentType(type)) continue;
|
|
39993
|
-
const
|
|
41186
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
41187
|
+
const canonical = toCanonicalToolName$7(entry.toolName);
|
|
39994
41188
|
if (forbiddenMapKeys.has(canonical)) {
|
|
39995
41189
|
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.`);
|
|
39996
41190
|
continue;
|
|
39997
41191
|
}
|
|
39998
41192
|
const action = augmentTypeToAction(type);
|
|
39999
41193
|
let pattern;
|
|
40000
|
-
if (
|
|
41194
|
+
if (legacyToolName === "launch-process" && entry.shellInputRegex) {
|
|
40001
41195
|
const regex = entry.shellInputRegex;
|
|
40002
41196
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
40003
41197
|
else if (action === "deny") {
|
|
@@ -40012,9 +41206,19 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40012
41206
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because its pattern resolves to the reserved object key '${pattern}'.`);
|
|
40013
41207
|
continue;
|
|
40014
41208
|
}
|
|
40015
|
-
|
|
40016
|
-
|
|
40017
|
-
|
|
41209
|
+
let bucket = lookupOwn({
|
|
41210
|
+
record: permission,
|
|
41211
|
+
key: canonical
|
|
41212
|
+
});
|
|
41213
|
+
if (bucket === void 0) {
|
|
41214
|
+
bucket = {};
|
|
41215
|
+
permission[canonical] = bucket;
|
|
41216
|
+
}
|
|
41217
|
+
const existing = lookupOwn({
|
|
41218
|
+
record: bucket,
|
|
41219
|
+
key: pattern
|
|
41220
|
+
});
|
|
41221
|
+
if (existing === void 0 || actionPriority[action] > actionPriority[existing]) bucket[pattern] = action;
|
|
40018
41222
|
}
|
|
40019
41223
|
return { permission };
|
|
40020
41224
|
}
|
|
@@ -40225,7 +41429,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
40225
41429
|
function toClaudeToolName(canonical) {
|
|
40226
41430
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
40227
41431
|
}
|
|
40228
|
-
function toCanonicalToolName$
|
|
41432
|
+
function toCanonicalToolName$6(claudeName) {
|
|
40229
41433
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
40230
41434
|
}
|
|
40231
41435
|
/**
|
|
@@ -41120,7 +42324,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
41120
42324
|
const processEntries = (entries, action) => {
|
|
41121
42325
|
for (const entry of entries) {
|
|
41122
42326
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
41123
|
-
const canonical = toCanonicalToolName$
|
|
42327
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
41124
42328
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41125
42329
|
permission[canonical][pattern] = action;
|
|
41126
42330
|
}
|
|
@@ -41943,9 +43147,634 @@ function mapBashActionToDecision(action) {
|
|
|
41943
43147
|
return "forbidden";
|
|
41944
43148
|
}
|
|
41945
43149
|
//#endregion
|
|
43150
|
+
//#region src/features/permissions/commandcode-permissions.ts
|
|
43151
|
+
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
43152
|
+
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
43153
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
43154
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
43155
|
+
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
43156
|
+
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
43157
|
+
bash: "Shell",
|
|
43158
|
+
read: "Read",
|
|
43159
|
+
edit: "Edit",
|
|
43160
|
+
write: "Write",
|
|
43161
|
+
grep: "Grep",
|
|
43162
|
+
glob: "Glob",
|
|
43163
|
+
webfetch: "WebFetch",
|
|
43164
|
+
websearch: "WebSearch"
|
|
43165
|
+
};
|
|
43166
|
+
const COMMANDCODE_TOOL_TO_CATEGORY = {
|
|
43167
|
+
...Object.fromEntries(Object.entries(CATEGORY_TO_COMMANDCODE_TOOL).map(([category, tool]) => [tool.toLowerCase(), category])),
|
|
43168
|
+
bash: "bash",
|
|
43169
|
+
powershell: "bash",
|
|
43170
|
+
shell_command: "bash",
|
|
43171
|
+
monitor_command: "bash",
|
|
43172
|
+
kill_shell: "bash",
|
|
43173
|
+
notebookedit: "edit",
|
|
43174
|
+
write_file: "write",
|
|
43175
|
+
web_fetch: "webfetch",
|
|
43176
|
+
web_search: "websearch"
|
|
43177
|
+
};
|
|
43178
|
+
const ACTION_RANK$1 = {
|
|
43179
|
+
allow: 0,
|
|
43180
|
+
ask: 1,
|
|
43181
|
+
deny: 2
|
|
43182
|
+
};
|
|
43183
|
+
/** Whether `action` outranks `existing` (deny > ask > allow). */
|
|
43184
|
+
function isStricterAction({ action, existing }) {
|
|
43185
|
+
return existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing];
|
|
43186
|
+
}
|
|
43187
|
+
/**
|
|
43188
|
+
* Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
|
|
43189
|
+
* the whitespace makes it a pattern rule, and a shell pattern grants only
|
|
43190
|
+
* commands that pass Command Code's pattern gate.
|
|
43191
|
+
*/
|
|
43192
|
+
const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (the command must parse into words and carry no environment assignment)";
|
|
43193
|
+
/**
|
|
43194
|
+
* Build a Command Code rule (`Shell(git *)`, `Read`, `mcp__github__get_issue`,
|
|
43195
|
+
* `mcp__*`, `*`) from a canonical category + pattern. Returns `null` for
|
|
43196
|
+
* categories Command Code cannot express so the caller can skip them.
|
|
43197
|
+
*
|
|
43198
|
+
* MCP tools are keyed by their canonical `mcp__server__tool` name, so a
|
|
43199
|
+
* scoped `mcp__<remainder>` category is written verbatim and the bare `mcp`
|
|
43200
|
+
* category becomes `mcp__*` (or `mcp__<pattern>` when the pattern names a
|
|
43201
|
+
* server or tool). A pattern on an MCP category is still written as the
|
|
43202
|
+
* `(specifier)` here; `writableCommandcodeRule` decides what to do with it,
|
|
43203
|
+
* because Command Code ignores it. The all-tools `*` category is only
|
|
43204
|
+
* written for its catch-all pattern; narrower `*` patterns are shell
|
|
43205
|
+
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
43206
|
+
*/
|
|
43207
|
+
function buildCommandcodeRule(category, pattern) {
|
|
43208
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
|
|
43209
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
|
|
43210
|
+
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
|
|
43211
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
|
|
43212
|
+
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
43213
|
+
if (tool === void 0) return null;
|
|
43214
|
+
return catchAll ? tool : `${tool}(${pattern})`;
|
|
43215
|
+
}
|
|
43216
|
+
/**
|
|
43217
|
+
* Split `Tool(specifier)` into its two halves the way Command Code's
|
|
43218
|
+
* `splitRule` does (`command-code` 1.54.0): the rule is split at the first
|
|
43219
|
+
* unescaped `(` and must end with `)`, the tool half is trimmed (so
|
|
43220
|
+
* `Shell (rm -rf *)` is a `Shell` rule), and a `\(` / `\)` in the specifier
|
|
43221
|
+
* is an escaped parenthesis. `Tool` alone has an empty specifier. A rule that
|
|
43222
|
+
* opens a parenthesis without closing one is not a rule to Command Code at
|
|
43223
|
+
* all, so it comes back with an empty tool half and stays unmodeled. Nothing
|
|
43224
|
+
* else in the specifier is touched: Command Code does not trim it (the shell
|
|
43225
|
+
* matcher normalizes whitespace on its own, see `parseCommandcodeRule`), so
|
|
43226
|
+
* `Read( )` is a rule for the pattern `" "`, which matches nothing, not for
|
|
43227
|
+
* the whole tool.
|
|
43228
|
+
*/
|
|
43229
|
+
function splitCommandcodeRule(rule) {
|
|
43230
|
+
const trimmed = rule.trim();
|
|
43231
|
+
const parenIndex = trimmed.search(/(?<!\\)\(/);
|
|
43232
|
+
if (parenIndex === -1) return {
|
|
43233
|
+
tool: trimmed,
|
|
43234
|
+
inner: ""
|
|
43235
|
+
};
|
|
43236
|
+
if (!trimmed.endsWith(")")) return {
|
|
43237
|
+
tool: "",
|
|
43238
|
+
inner: ""
|
|
43239
|
+
};
|
|
43240
|
+
return {
|
|
43241
|
+
tool: trimmed.slice(0, parenIndex).trim(),
|
|
43242
|
+
inner: trimmed.slice(parenIndex + 1, -1).replace(/\\([()])/g, "$1")
|
|
43243
|
+
};
|
|
43244
|
+
}
|
|
43245
|
+
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
43246
|
+
function isCatchAllSpecifier(inner) {
|
|
43247
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$6;
|
|
43248
|
+
}
|
|
43249
|
+
/**
|
|
43250
|
+
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
43251
|
+
* `parsePermissionRule` / `parseMcpToken`). The MCP shape is only recognized
|
|
43252
|
+
* by its exact `mcp__` prefix, and such a rule never carries a specifier:
|
|
43253
|
+
* `mcp__<server>__<tool>(owner:foo)` denies, asks or allows the whole tool.
|
|
43254
|
+
* Its names are matched case-sensitively against the registered server and
|
|
43255
|
+
* tool (Command Code never lowercases them), so the remainder keeps its case
|
|
43256
|
+
* as the canonical category. `mcp__<server>`, `mcp__<server>__` and
|
|
43257
|
+
* `mcp__<server>__*` are the whole server (category `mcp__<server>`), and a
|
|
43258
|
+
* `*` server (`mcp__*`, `mcp__*__<tool>`) is every MCP tool, honored in
|
|
43259
|
+
* `deny`/`ask` only. A differently-cased `MCP__...` spelling is read as a
|
|
43260
|
+
* plain tool-name rule instead, matched case-insensitively:
|
|
43261
|
+
* `MCP__<server>__<tool>` matches that tool by name (so it folds onto the
|
|
43262
|
+
* `mcp__` category, remainder as written), `MCP__<server>__<tool>(specifier)`
|
|
43263
|
+
* globs the specifier against the call's `command` / `file_path` / `path` /
|
|
43264
|
+
* `url` / `pattern` argument (in `deny`/`ask` a `param:glob` form is tried
|
|
43265
|
+
* first) — kept as a pattern in `deny`/`ask`, but not imported from `allow`,
|
|
43266
|
+
* where a grant scoped that way cannot be modeled without widening it; the
|
|
43267
|
+
* globs
|
|
43268
|
+
* `MCP__*` (every MCP tool) and `MCP__<server>__*` (the whole server) match
|
|
43269
|
+
* by name in `deny`/`ask` only, and `MCP__<server>` names no tool at all.
|
|
43270
|
+
* Whatever matches nothing anywhere (or a glob rulesync does not model)
|
|
43271
|
+
* returns `null` and is left alone.
|
|
43272
|
+
*/
|
|
43273
|
+
function parseMcpCommandcodeRule({ tool, inner }) {
|
|
43274
|
+
const remainder = tool.slice(5);
|
|
43275
|
+
const separator = remainder.indexOf("__");
|
|
43276
|
+
const server = separator === -1 ? remainder : remainder.slice(0, separator);
|
|
43277
|
+
const rest = separator === -1 ? "" : remainder.slice(separator + 2);
|
|
43278
|
+
if (server.length === 0) return null;
|
|
43279
|
+
const everyServer = {
|
|
43280
|
+
category: "mcp",
|
|
43281
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43282
|
+
notInAllow: true
|
|
43283
|
+
};
|
|
43284
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
|
|
43285
|
+
if (server === CATCH_ALL_PATTERN$6) return everyServer;
|
|
43286
|
+
if (server.includes(CATCH_ALL_PATTERN$6)) return null;
|
|
43287
|
+
return {
|
|
43288
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
|
|
43289
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43290
|
+
notInAllow: false
|
|
43291
|
+
};
|
|
43292
|
+
}
|
|
43293
|
+
const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
|
|
43294
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
|
|
43295
|
+
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
43296
|
+
category,
|
|
43297
|
+
pattern: inner,
|
|
43298
|
+
notInAllow: true
|
|
43299
|
+
} : null;
|
|
43300
|
+
if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
|
|
43301
|
+
if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
|
|
43302
|
+
category: `${MCP_CANONICAL_PREFIX$5}${server}`,
|
|
43303
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43304
|
+
notInAllow: true
|
|
43305
|
+
};
|
|
43306
|
+
return namesTool ? {
|
|
43307
|
+
category,
|
|
43308
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43309
|
+
notInAllow: false
|
|
43310
|
+
} : null;
|
|
43311
|
+
}
|
|
43312
|
+
/**
|
|
43313
|
+
* A rule on one of the friendly tools, with its specifier read as Command
|
|
43314
|
+
* Code matches it. The shell matcher trims and collapses whitespace on both
|
|
43315
|
+
* the pattern and the command, so `Shell( git * )` is `Shell(git *)` and a
|
|
43316
|
+
* pattern that is blank once normalized (`Shell( )`), or a bare `:*` whose
|
|
43317
|
+
* prefix is empty (`Shell(:*)`), matches nothing. A padded `Shell( * )` is
|
|
43318
|
+
* not the bare `Shell`: only a specifier of exactly
|
|
43319
|
+
* `` or `*` is dropped by the rule parser, so `( * )` stays a pattern rule —
|
|
43320
|
+
* in `deny`/`ask` it matches every command (the whole tool), while in `allow`
|
|
43321
|
+
* it is narrower than the bare `Shell` (it needs a command that parses into
|
|
43322
|
+
* words and has no environment-assignment prefix), so it is the whole tool
|
|
43323
|
+
* marked `notInAllow`. The path, web and tool-name matchers use the
|
|
43324
|
+
* specifier as written, so one that is blank or `*` only once trimmed
|
|
43325
|
+
* (`Read( * )`) matches nothing. What matches nothing comes back as `null`:
|
|
43326
|
+
* the rule is unmodeled and left alone.
|
|
43327
|
+
*/
|
|
43328
|
+
function friendlyToolRule({ category, inner }) {
|
|
43329
|
+
if (category === "bash") {
|
|
43330
|
+
if (isCatchAllSpecifier(inner)) return {
|
|
43331
|
+
category,
|
|
43332
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43333
|
+
notInAllow: false
|
|
43334
|
+
};
|
|
43335
|
+
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
43336
|
+
if (normalized === "" || normalized === ":*") return null;
|
|
43337
|
+
return isCatchAllSpecifier(normalized) ? {
|
|
43338
|
+
category,
|
|
43339
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43340
|
+
notInAllow: true
|
|
43341
|
+
} : {
|
|
43342
|
+
category,
|
|
43343
|
+
pattern: normalized,
|
|
43344
|
+
notInAllow: false
|
|
43345
|
+
};
|
|
43346
|
+
}
|
|
43347
|
+
if (isCatchAllSpecifier(inner)) return {
|
|
43348
|
+
category,
|
|
43349
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43350
|
+
notInAllow: false
|
|
43351
|
+
};
|
|
43352
|
+
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
43353
|
+
category,
|
|
43354
|
+
pattern: inner,
|
|
43355
|
+
notInAllow: false
|
|
43356
|
+
};
|
|
43357
|
+
}
|
|
43358
|
+
/**
|
|
43359
|
+
* Parse a Command Code rule back into canonical terms. Tool names fold case;
|
|
43360
|
+
* `Tool`, `Tool()` and `Tool(*)` all mean the whole tool; MCP-shaped rules
|
|
43361
|
+
* go through `parseMcpCommandcodeRule`. Returns `null` for a rule rulesync
|
|
43362
|
+
* cannot model (an exact internal tool name such as `edit_file`, a
|
|
43363
|
+
* name-wildcard like `edit_*`, a specifier on a rule that takes none, or a
|
|
43364
|
+
* rule Command Code enforces nothing for, such as `Agent`).
|
|
43365
|
+
*/
|
|
43366
|
+
function parseCommandcodeRule(rule) {
|
|
43367
|
+
const { tool, inner } = splitCommandcodeRule(rule);
|
|
43368
|
+
if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
|
|
43369
|
+
category: "*",
|
|
43370
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
43371
|
+
notInAllow: true
|
|
43372
|
+
} : null;
|
|
43373
|
+
const lowered = tool.toLowerCase();
|
|
43374
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
|
|
43375
|
+
tool,
|
|
43376
|
+
inner
|
|
43377
|
+
});
|
|
43378
|
+
const category = Object.hasOwn(COMMANDCODE_TOOL_TO_CATEGORY, lowered) ? COMMANDCODE_TOOL_TO_CATEGORY[lowered] : void 0;
|
|
43379
|
+
if (category === void 0) return null;
|
|
43380
|
+
return friendlyToolRule({
|
|
43381
|
+
category,
|
|
43382
|
+
inner
|
|
43383
|
+
});
|
|
43384
|
+
}
|
|
43385
|
+
/**
|
|
43386
|
+
* Whether an imported rule is a differently-cased `MCP__<server>__<tool>`
|
|
43387
|
+
* name that folded onto an exact-prefix `mcp__` category (which Command Code
|
|
43388
|
+
* matches case-sensitively, unlike the name rule it came from).
|
|
43389
|
+
*/
|
|
43390
|
+
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
43391
|
+
const { tool } = splitCommandcodeRule(rule);
|
|
43392
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
|
|
43393
|
+
}
|
|
43394
|
+
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
43395
|
+
function scopedMcpRuleTool(rule) {
|
|
43396
|
+
const { tool, inner } = splitCommandcodeRule(rule);
|
|
43397
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
|
|
43398
|
+
}
|
|
43399
|
+
/**
|
|
43400
|
+
* The string entries of a Command Code permission list. Command Code reads
|
|
43401
|
+
* each entry on its own and skips one that is not a string, so a stray
|
|
43402
|
+
* `null` beside `Shell(rm -rf *)` must not throw the deny away with it; the
|
|
43403
|
+
* skipped entries are counted in a warning because they cannot be kept.
|
|
43404
|
+
*/
|
|
43405
|
+
function stringRules({ list, action, outcome, warn }) {
|
|
43406
|
+
if (!Array.isArray(list)) return [];
|
|
43407
|
+
const rules = list.filter((entry) => typeof entry === "string");
|
|
43408
|
+
const skipped = list.length - rules.length;
|
|
43409
|
+
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}.`);
|
|
43410
|
+
return rules;
|
|
43411
|
+
}
|
|
43412
|
+
/**
|
|
43413
|
+
* The canonical categories this run rebuilds — every category the canonical
|
|
43414
|
+
* config names (an empty `bash: {}` still reclaims the previous `Shell(...)`
|
|
43415
|
+
* entries, as in the Claude Code adapter) plus every rule it emits, keyed the
|
|
43416
|
+
* way an existing entry parses back (so `mcp: { github: "deny" }`, written as
|
|
43417
|
+
* `mcp__github`, claims the `mcp__github` entries of the previous run; case
|
|
43418
|
+
* variants of the friendly tool names fold together, while MCP names are
|
|
43419
|
+
* matched exactly, as Command Code does). An existing entry whose tool folds onto one of them is
|
|
43420
|
+
* rulesync's to replace, whatever list it sits in — otherwise flipping a rule
|
|
43421
|
+
* from deny to allow would leave the old deny behind and win. Every other
|
|
43422
|
+
* entry — an internal tool name rulesync cannot model, or a modeled tool the
|
|
43423
|
+
* canonical config does not mention — is the user's (Command Code writes
|
|
43424
|
+
* interactive approvals into the same lists) and is preserved verbatim.
|
|
43425
|
+
* A `deny`/`ask` entry that is reclaimed without an equally strict rule
|
|
43426
|
+
* taking its place (say a hand-written `mcp__*` deny next to a canonical
|
|
43427
|
+
* `mcp: { github: "deny" }`) is reported, so regenerating never silently
|
|
43428
|
+
* loosens what the user wrote. Mirrors `managedClaudeToolNames` in the
|
|
43429
|
+
* Claude Code adapter.
|
|
43430
|
+
*/
|
|
43431
|
+
function preservedRules({ existingPermissions, key, managedCategories, written, logger }) {
|
|
43432
|
+
return stringRules({
|
|
43433
|
+
list: existingPermissions[key],
|
|
43434
|
+
action: key,
|
|
43435
|
+
outcome: "dropped from the regenerated list",
|
|
43436
|
+
warn: (message) => logger?.warn(message)
|
|
43437
|
+
}).filter((rule) => {
|
|
43438
|
+
const parsed = parseCommandcodeRule(rule);
|
|
43439
|
+
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
43440
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
|
|
43441
|
+
if (key !== "allow" && isStricterAction({
|
|
43442
|
+
action: key,
|
|
43443
|
+
existing: replacement
|
|
43444
|
+
})) 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.`);
|
|
43445
|
+
return false;
|
|
43446
|
+
});
|
|
43447
|
+
}
|
|
43448
|
+
/**
|
|
43449
|
+
* The rule to write for a canonical rule, or `null` when Command Code would
|
|
43450
|
+
* ignore it in the `action` list. A server-less wildcard (`*`, `mcp__*`) in
|
|
43451
|
+
* `allow` is warned about and left unwritten rather than written dead. A
|
|
43452
|
+
* specifier on an MCP rule is ignored by Command Code in every list, so a
|
|
43453
|
+
* scoped `deny`/`ask` is written as the bare tool (which is what Command
|
|
43454
|
+
* Code would enforce anyway, only now visibly) with a warning, and a scoped
|
|
43455
|
+
* `allow` — which would grant the whole tool — is refused. Judged on the
|
|
43456
|
+
* rule as written, so the bare `mcp` category cannot smuggle a specifier in
|
|
43457
|
+
* through a `<server>__<tool>(specifier)` pattern.
|
|
43458
|
+
*/
|
|
43459
|
+
function writableCommandcodeRule({ rule, emitted, action, logger }) {
|
|
43460
|
+
if (action === "allow" && emitted.notInAllow) {
|
|
43461
|
+
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.`);
|
|
43462
|
+
return null;
|
|
43463
|
+
}
|
|
43464
|
+
const tool = scopedMcpRuleTool(rule);
|
|
43465
|
+
if (tool === null) return rule;
|
|
43466
|
+
if (action === "allow") {
|
|
43467
|
+
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.`);
|
|
43468
|
+
return null;
|
|
43469
|
+
}
|
|
43470
|
+
logger?.warn(`Command Code ignores the specifier of an MCP rule, so '${rule}' was written as '${tool}' and applies "${action}" to the whole tool.`);
|
|
43471
|
+
return tool;
|
|
43472
|
+
}
|
|
43473
|
+
/**
|
|
43474
|
+
* Record `action` for `rule`, keeping the stricter one (deny > ask > allow)
|
|
43475
|
+
* when two canonical rules collapse onto the same Command Code entry.
|
|
43476
|
+
*/
|
|
43477
|
+
function rankCommandcodeRule({ ranked, rule, action, logger }) {
|
|
43478
|
+
const existing = ranked.get(rule);
|
|
43479
|
+
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).`);
|
|
43480
|
+
if (isStricterAction({
|
|
43481
|
+
action,
|
|
43482
|
+
existing
|
|
43483
|
+
})) ranked.set(rule, action);
|
|
43484
|
+
}
|
|
43485
|
+
/**
|
|
43486
|
+
* Bucket the canonical rules into Command Code's `allow`/`ask`/`deny` lists.
|
|
43487
|
+
* Collisions resolve to the strictest action (deny > ask > allow); categories
|
|
43488
|
+
* Command Code cannot express are skipped with a warning when they carry a
|
|
43489
|
+
* `deny`; rules Command Code would ignore in the target list are dropped or
|
|
43490
|
+
* rewritten by `writableCommandcodeRule`.
|
|
43491
|
+
*/
|
|
43492
|
+
function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
43493
|
+
const permission = honorAllToolsOnBash(config.permission);
|
|
43494
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
43495
|
+
const managedCategories = /* @__PURE__ */ new Set();
|
|
43496
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
43497
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
|
|
43498
|
+
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
43499
|
+
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
43500
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
43501
|
+
const rule = buildCommandcodeRule(category, pattern);
|
|
43502
|
+
const emitted = rule === null ? null : parseCommandcodeRule(rule);
|
|
43503
|
+
if (rule === null || emitted === null) {
|
|
43504
|
+
const honoredOnShell = category === "*" && permission.bash !== void 0;
|
|
43505
|
+
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.`);
|
|
43506
|
+
continue;
|
|
43507
|
+
}
|
|
43508
|
+
managedCategories.add(emitted.category);
|
|
43509
|
+
const written = writableCommandcodeRule({
|
|
43510
|
+
rule,
|
|
43511
|
+
emitted,
|
|
43512
|
+
action,
|
|
43513
|
+
logger
|
|
43514
|
+
});
|
|
43515
|
+
if (written !== null) rankCommandcodeRule({
|
|
43516
|
+
ranked,
|
|
43517
|
+
rule: written,
|
|
43518
|
+
action,
|
|
43519
|
+
logger
|
|
43520
|
+
});
|
|
43521
|
+
}
|
|
43522
|
+
}
|
|
43523
|
+
const written = /* @__PURE__ */ new Map();
|
|
43524
|
+
for (const [rule, action] of ranked) {
|
|
43525
|
+
const parsed = parseCommandcodeRule(rule);
|
|
43526
|
+
if (parsed !== null) written.set(`${parsed.category}(${parsed.pattern})`, action);
|
|
43527
|
+
}
|
|
43528
|
+
const preserved = {
|
|
43529
|
+
existingPermissions,
|
|
43530
|
+
managedCategories,
|
|
43531
|
+
written,
|
|
43532
|
+
logger
|
|
43533
|
+
};
|
|
43534
|
+
const allow = preservedRules({
|
|
43535
|
+
...preserved,
|
|
43536
|
+
key: "allow"
|
|
43537
|
+
});
|
|
43538
|
+
const ask = preservedRules({
|
|
43539
|
+
...preserved,
|
|
43540
|
+
key: "ask"
|
|
43541
|
+
});
|
|
43542
|
+
const deny = preservedRules({
|
|
43543
|
+
...preserved,
|
|
43544
|
+
key: "deny"
|
|
43545
|
+
});
|
|
43546
|
+
for (const [rule, action] of ranked) if (action === "allow") allow.push(rule);
|
|
43547
|
+
else if (action === "ask") ask.push(rule);
|
|
43548
|
+
else deny.push(rule);
|
|
43549
|
+
return {
|
|
43550
|
+
allow: uniq(allow.toSorted()),
|
|
43551
|
+
ask: uniq(ask.toSorted()),
|
|
43552
|
+
deny: uniq(deny.toSorted())
|
|
43553
|
+
};
|
|
43554
|
+
}
|
|
43555
|
+
/**
|
|
43556
|
+
* Why a rule marked `notInAllow` is not imported from `allow`: Command Code
|
|
43557
|
+
* ignores a server-less wildcard or a tool-name glob there, while it does
|
|
43558
|
+
* enforce a scoped `MCP__<server>__<tool>(specifier)` — as a glob over the
|
|
43559
|
+
* call's arguments — and a padded `Shell( * )` — as a pattern narrower than
|
|
43560
|
+
* the bare `Shell` — which rulesync cannot model in `allow` without widening
|
|
43561
|
+
* the grant.
|
|
43562
|
+
*/
|
|
43563
|
+
function skippedAllowImportMessage({ rule, category }) {
|
|
43564
|
+
const { tool, inner } = splitCommandcodeRule(rule);
|
|
43565
|
+
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.`;
|
|
43566
|
+
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.`;
|
|
43567
|
+
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.`;
|
|
43568
|
+
}
|
|
43569
|
+
/**
|
|
43570
|
+
* Parse Command Code's `permissions` lists back into a canonical permission
|
|
43571
|
+
* map with `deny > ask > allow` precedence, so a tool described more than
|
|
43572
|
+
* once resolves to the strictest action. Each rule is imported as what
|
|
43573
|
+
* Command Code enforces for it, never as what it looks like: a rule Command
|
|
43574
|
+
* Code ignores in `allow` (`*`, `mcp__*`, an `MCP__...` glob) is skipped
|
|
43575
|
+
* there with a warning, since importing it would turn a dead line into a
|
|
43576
|
+
* live grant for every other target, and so is a scoped
|
|
43577
|
+
* `MCP__<server>__<tool>(specifier)` allow, which Command Code enforces as a
|
|
43578
|
+
* glob over the call's arguments but rulesync cannot import without widening
|
|
43579
|
+
* it to the whole tool; the specifier of an exact-prefix `mcp__...` rule,
|
|
43580
|
+
* which Command Code drops, is folded to the whole tool in every list — with
|
|
43581
|
+
* a warning in `allow`, where the fold widens the grant the user wrote. A
|
|
43582
|
+
* tool-half glob in that exact-prefix spelling (`mcp__<server>__get_*`), which
|
|
43583
|
+
* Command Code matches against tool names in every list, is imported as the
|
|
43584
|
+
* category spelled that way, the same one the generator writes back; other
|
|
43585
|
+
* targets read it as a literal tool name.
|
|
43586
|
+
*/
|
|
43587
|
+
function parseCommandcodeRuleLists(permissions) {
|
|
43588
|
+
const permission = {};
|
|
43589
|
+
const lists = [
|
|
43590
|
+
["allow", permissions.allow],
|
|
43591
|
+
["ask", permissions.ask],
|
|
43592
|
+
["deny", permissions.deny]
|
|
43593
|
+
];
|
|
43594
|
+
for (const [action, rawList] of lists) {
|
|
43595
|
+
const list = stringRules({
|
|
43596
|
+
list: rawList,
|
|
43597
|
+
action,
|
|
43598
|
+
outcome: "not imported",
|
|
43599
|
+
warn: (message) => fallbackLogger.warn(message)
|
|
43600
|
+
});
|
|
43601
|
+
for (const rule of list) {
|
|
43602
|
+
const parsed = parseCommandcodeRule(rule);
|
|
43603
|
+
if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) {
|
|
43604
|
+
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.`);
|
|
43605
|
+
continue;
|
|
43606
|
+
}
|
|
43607
|
+
const { category, pattern } = parsed;
|
|
43608
|
+
if (action === "allow" && parsed.notInAllow) {
|
|
43609
|
+
fallbackLogger.warn(skippedAllowImportMessage({
|
|
43610
|
+
rule,
|
|
43611
|
+
category
|
|
43612
|
+
}));
|
|
43613
|
+
continue;
|
|
43614
|
+
}
|
|
43615
|
+
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.`);
|
|
43616
|
+
if (isFoldedMcpToolNameRule({
|
|
43617
|
+
rule,
|
|
43618
|
+
category
|
|
43619
|
+
})) 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.`);
|
|
43620
|
+
const rules = permission[category] ??= {};
|
|
43621
|
+
if (isStricterAction({
|
|
43622
|
+
action,
|
|
43623
|
+
existing: rules[pattern]
|
|
43624
|
+
})) rules[pattern] = action;
|
|
43625
|
+
}
|
|
43626
|
+
}
|
|
43627
|
+
return permission;
|
|
43628
|
+
}
|
|
43629
|
+
/**
|
|
43630
|
+
* Permissions generator for Command Code.
|
|
43631
|
+
*
|
|
43632
|
+
* Rules live under the `permissions` key of `.commandcode/settings.json`
|
|
43633
|
+
* (project) and `~/.commandcode/settings.json` (user) as three lists of
|
|
43634
|
+
* Claude-style entries: `deny` (always wins), `ask`, `allow`. The same key
|
|
43635
|
+
* also carries `defaultMode`, `additionalDirectories` and `disableBypass`,
|
|
43636
|
+
* and the file holds `hooks` and other settings, so writes go through the
|
|
43637
|
+
* shared-config gateway: rulesync replaces the three lists, keeps every
|
|
43638
|
+
* sibling key, and never deletes the file.
|
|
43639
|
+
*
|
|
43640
|
+
* Generate: `permission.<category>.<pattern>` becomes `Tool(pattern)` with
|
|
43641
|
+
* the friendly names (`Shell`, `Read`, `Edit`, `Write`, `WebFetch`,
|
|
43642
|
+
* `WebSearch`), `mcp__<server>__<tool>` names pass through, and the all-tools
|
|
43643
|
+
* `*` category becomes the bare `*` rule Command Code accepts in `deny`/`ask`.
|
|
43644
|
+
* Only the entries of the categories the canonical config names are rebuilt;
|
|
43645
|
+
* every other existing entry is preserved verbatim.
|
|
43646
|
+
* Import: the lists are parsed back as what Command Code enforces for each
|
|
43647
|
+
* entry — friendly tool names case-insensitively (with the internal aliases
|
|
43648
|
+
* such as `Bash`, `PowerShell` and `write_file` folding onto their
|
|
43649
|
+
* category), MCP names exactly; rules for internal tool names rulesync does
|
|
43650
|
+
* not model are skipped.
|
|
43651
|
+
*
|
|
43652
|
+
* @see https://commandcode.ai/docs/permissions
|
|
43653
|
+
* @see https://commandcode.ai/docs/settings
|
|
43654
|
+
*/
|
|
43655
|
+
var CommandcodePermissions = class CommandcodePermissions extends ToolPermissions {
|
|
43656
|
+
constructor(params) {
|
|
43657
|
+
super({
|
|
43658
|
+
...params,
|
|
43659
|
+
fileContent: params.fileContent ?? "{}"
|
|
43660
|
+
});
|
|
43661
|
+
}
|
|
43662
|
+
/**
|
|
43663
|
+
* `settings.json` holds hooks and other user settings, so it is never
|
|
43664
|
+
* deleted; clearing permissions happens via an in-place merge.
|
|
43665
|
+
*/
|
|
43666
|
+
isDeletable() {
|
|
43667
|
+
return false;
|
|
43668
|
+
}
|
|
43669
|
+
static getSettablePaths(_options = {}) {
|
|
43670
|
+
return {
|
|
43671
|
+
relativeDirPath: COMMANDCODE_DIR,
|
|
43672
|
+
relativeFilePath: COMMANDCODE_SETTINGS_FILE_NAME
|
|
43673
|
+
};
|
|
43674
|
+
}
|
|
43675
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
43676
|
+
const paths = CommandcodePermissions.getSettablePaths({ global });
|
|
43677
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
43678
|
+
return new CommandcodePermissions({
|
|
43679
|
+
outputRoot,
|
|
43680
|
+
relativeDirPath: paths.relativeDirPath,
|
|
43681
|
+
relativeFilePath: paths.relativeFilePath,
|
|
43682
|
+
fileContent,
|
|
43683
|
+
validate,
|
|
43684
|
+
global
|
|
43685
|
+
});
|
|
43686
|
+
}
|
|
43687
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
|
|
43688
|
+
const paths = CommandcodePermissions.getSettablePaths({ global });
|
|
43689
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43690
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
43691
|
+
const existing = parseCommandcodeSettings({
|
|
43692
|
+
fileContent: existingContent,
|
|
43693
|
+
relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
|
|
43694
|
+
});
|
|
43695
|
+
const existingPermissions = isRecord$1(existing[COMMANDCODE_PERMISSIONS_KEY]) ? existing[COMMANDCODE_PERMISSIONS_KEY] : {};
|
|
43696
|
+
const lists = buildCommandcodeRuleLists({
|
|
43697
|
+
config: rulesyncPermissions.getJson(),
|
|
43698
|
+
existingPermissions,
|
|
43699
|
+
logger
|
|
43700
|
+
});
|
|
43701
|
+
const permissions = { ...existingPermissions };
|
|
43702
|
+
for (const key of [
|
|
43703
|
+
"allow",
|
|
43704
|
+
"ask",
|
|
43705
|
+
"deny"
|
|
43706
|
+
]) if (lists[key].length > 0) permissions[key] = lists[key];
|
|
43707
|
+
else delete permissions[key];
|
|
43708
|
+
return new CommandcodePermissions({
|
|
43709
|
+
outputRoot,
|
|
43710
|
+
relativeDirPath: paths.relativeDirPath,
|
|
43711
|
+
relativeFilePath: paths.relativeFilePath,
|
|
43712
|
+
fileContent: applySharedConfigPatch({
|
|
43713
|
+
fileKey: sharedConfigFileKey(paths),
|
|
43714
|
+
feature: "permissions",
|
|
43715
|
+
existingContent,
|
|
43716
|
+
patch: { [COMMANDCODE_PERMISSIONS_KEY]: permissions },
|
|
43717
|
+
filePath
|
|
43718
|
+
}),
|
|
43719
|
+
validate: true,
|
|
43720
|
+
global
|
|
43721
|
+
});
|
|
43722
|
+
}
|
|
43723
|
+
toRulesyncPermissions() {
|
|
43724
|
+
const settings = parseCommandcodeSettings({
|
|
43725
|
+
fileContent: this.getFileContent() || "{}",
|
|
43726
|
+
relativePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
43727
|
+
});
|
|
43728
|
+
const permissions = isRecord$1(settings[COMMANDCODE_PERMISSIONS_KEY]) ? settings[COMMANDCODE_PERMISSIONS_KEY] : {};
|
|
43729
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission: parseCommandcodeRuleLists(permissions) }, null, 2) });
|
|
43730
|
+
}
|
|
43731
|
+
validate() {
|
|
43732
|
+
return {
|
|
43733
|
+
success: true,
|
|
43734
|
+
error: null
|
|
43735
|
+
};
|
|
43736
|
+
}
|
|
43737
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
43738
|
+
return new CommandcodePermissions({
|
|
43739
|
+
outputRoot,
|
|
43740
|
+
relativeDirPath,
|
|
43741
|
+
relativeFilePath,
|
|
43742
|
+
fileContent: "{}",
|
|
43743
|
+
validate: false,
|
|
43744
|
+
global
|
|
43745
|
+
});
|
|
43746
|
+
}
|
|
43747
|
+
};
|
|
43748
|
+
/**
|
|
43749
|
+
* Fail closed on a syntax error or non-object root, matching the write path's
|
|
43750
|
+
* shared-config declaration, so a broken file is surfaced rather than
|
|
43751
|
+
* partially imported or overwritten.
|
|
43752
|
+
*/
|
|
43753
|
+
function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
43754
|
+
try {
|
|
43755
|
+
return parseSharedConfig({
|
|
43756
|
+
format: "json",
|
|
43757
|
+
fileContent,
|
|
43758
|
+
filePath: relativePath,
|
|
43759
|
+
invalidRootPolicy: "error"
|
|
43760
|
+
});
|
|
43761
|
+
} catch (error) {
|
|
43762
|
+
throw new Error(`Failed to parse Command Code settings in ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
43763
|
+
}
|
|
43764
|
+
}
|
|
43765
|
+
//#endregion
|
|
41946
43766
|
//#region src/features/permissions/continue-permissions.ts
|
|
41947
43767
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
41948
|
-
const CATCH_ALL_PATTERN$
|
|
43768
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
43769
|
+
/**
|
|
43770
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
43771
|
+
* would match `Tool(**)` against every argument just as the bare tool name
|
|
43772
|
+
* does, so it is written — and, under the all-tools category, honored — as
|
|
43773
|
+
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
43774
|
+
*/
|
|
43775
|
+
function normalizeCatchAllPattern$1(pattern) {
|
|
43776
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
43777
|
+
}
|
|
41949
43778
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
41950
43779
|
bash: "Bash",
|
|
41951
43780
|
read: "Read",
|
|
@@ -41973,7 +43802,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
41973
43802
|
function toContinueToolName(canonical) {
|
|
41974
43803
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
41975
43804
|
}
|
|
41976
|
-
function toCanonicalToolName$
|
|
43805
|
+
function toCanonicalToolName$5(continueName) {
|
|
41977
43806
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
41978
43807
|
}
|
|
41979
43808
|
/**
|
|
@@ -41981,7 +43810,7 @@ function toCanonicalToolName$4(continueName) {
|
|
|
41981
43810
|
* `Tool(pattern)`.
|
|
41982
43811
|
*/
|
|
41983
43812
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
41984
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
43813
|
+
return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
|
|
41985
43814
|
}
|
|
41986
43815
|
/**
|
|
41987
43816
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -41995,7 +43824,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
41995
43824
|
if (toolName === "") return;
|
|
41996
43825
|
return {
|
|
41997
43826
|
toolName,
|
|
41998
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
43827
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
|
|
41999
43828
|
};
|
|
42000
43829
|
}
|
|
42001
43830
|
/**
|
|
@@ -42162,13 +43991,14 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
42162
43991
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42163
43992
|
if (isPrototypePollutionKey(category)) continue;
|
|
42164
43993
|
const toolName = category === "*" ? "*" : toContinueToolName(category);
|
|
42165
|
-
for (const [
|
|
42166
|
-
if (isPrototypePollutionKey(
|
|
43994
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
43995
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
43996
|
+
const pattern = normalizeCatchAllPattern$1(rawPattern);
|
|
42167
43997
|
if (pattern.includes("(") || pattern.includes(")")) {
|
|
42168
43998
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42169
43999
|
continue;
|
|
42170
44000
|
}
|
|
42171
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
44001
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
|
|
42172
44002
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42173
44003
|
continue;
|
|
42174
44004
|
}
|
|
@@ -42193,7 +44023,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
42193
44023
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
42194
44024
|
if (parsedEntry === void 0) continue;
|
|
42195
44025
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
42196
|
-
const category = toCanonicalToolName$
|
|
44026
|
+
const category = toCanonicalToolName$5(parsedEntry.toolName);
|
|
42197
44027
|
permission[category] ??= {};
|
|
42198
44028
|
permission[category][parsedEntry.pattern] ??= action;
|
|
42199
44029
|
}
|
|
@@ -42516,6 +44346,295 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
42516
44346
|
}
|
|
42517
44347
|
};
|
|
42518
44348
|
//#endregion
|
|
44349
|
+
//#region src/features/permissions/crush-permissions.ts
|
|
44350
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
44351
|
+
/**
|
|
44352
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
44353
|
+
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
44354
|
+
* still be written.
|
|
44355
|
+
*/
|
|
44356
|
+
function normalizeCatchAllPattern(pattern) {
|
|
44357
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
|
|
44358
|
+
}
|
|
44359
|
+
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
44360
|
+
bash: "bash",
|
|
44361
|
+
read: "view",
|
|
44362
|
+
edit: "edit",
|
|
44363
|
+
write: "write",
|
|
44364
|
+
grep: "grep",
|
|
44365
|
+
glob: "glob",
|
|
44366
|
+
webfetch: "fetch"
|
|
44367
|
+
};
|
|
44368
|
+
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
44369
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
44370
|
+
const MCP_CRUSH_PREFIX = "mcp_";
|
|
44371
|
+
function toCrushToolName(canonical) {
|
|
44372
|
+
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
44373
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
44374
|
+
return canonical;
|
|
44375
|
+
}
|
|
44376
|
+
function toCanonicalToolName$4(crushName) {
|
|
44377
|
+
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
44378
|
+
}
|
|
44379
|
+
function isMcpToolName(crushName) {
|
|
44380
|
+
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
44381
|
+
}
|
|
44382
|
+
function uniq$1(values) {
|
|
44383
|
+
return [...new Set(values)];
|
|
44384
|
+
}
|
|
44385
|
+
/**
|
|
44386
|
+
* Permissions adapter for Crush.
|
|
44387
|
+
*
|
|
44388
|
+
* Crush has no per-argument permission rules. Its JSON config carries two
|
|
44389
|
+
* tool-level lists instead:
|
|
44390
|
+
* - `permissions.allowed_tools`: tools (or `tool:action` pairs) that run
|
|
44391
|
+
* without a permission prompt; everything else prompts.
|
|
44392
|
+
* - `options.disabled_tools`: built-in tools removed from the agent
|
|
44393
|
+
* entirely.
|
|
44394
|
+
*
|
|
44395
|
+
* Mapping (rulesync canonical -> Crush), catch-all (`*`) rules only:
|
|
44396
|
+
* - `allow` -> an `allowed_tools` entry.
|
|
44397
|
+
* - `deny` -> a `disabled_tools` entry. Crush only filters its built-ins
|
|
44398
|
+
* through that list, so a deny for an MCP tool is reported and skipped
|
|
44399
|
+
* (disable the tool in the MCP server's `disabled_tools` instead).
|
|
44400
|
+
* - `ask` -> nothing (prompting is Crush's default).
|
|
44401
|
+
* - Tool name: `bash` -> `bash`, `read` -> `view`, `edit` -> `edit`,
|
|
44402
|
+
* `write` -> `write`, `grep` -> `grep`, `glob` -> `glob`,
|
|
44403
|
+
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
44404
|
+
* any other category passes through verbatim.
|
|
44405
|
+
*
|
|
44406
|
+
* A pattern-specific rule cannot be expressed and is reported and skipped.
|
|
44407
|
+
* Because Crush cannot narrow an allowed tool, a category's catch-all `allow`
|
|
44408
|
+
* is not written while the same category carries a pattern-specific `deny` or
|
|
44409
|
+
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
44410
|
+
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
44411
|
+
* `honorAllToolsOnBash` and otherwise skipped, since Crush has no allow-all
|
|
44412
|
+
* or disable-all list.
|
|
44413
|
+
*
|
|
44414
|
+
* `crush.json` is Crush's main config: entries naming a tool the canonical
|
|
44415
|
+
* config does not manage, and every `tool:action` entry (a form rulesync
|
|
44416
|
+
* never derives), are preserved verbatim; the managed tools' bare entries
|
|
44417
|
+
* are rebuilt, every other key of `permissions` / `options` and of the file is
|
|
44418
|
+
* kept, and the file is never deleted. Project scope writes `crush.json`, or
|
|
44419
|
+
* an existing `.crush.json`; Crush merges the pair (lists concatenated), so an
|
|
44420
|
+
* entry left in the other file stays in effect and is reported.
|
|
44421
|
+
*
|
|
44422
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
44423
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
44424
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/permission/permission.go
|
|
44425
|
+
*/
|
|
44426
|
+
var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
44427
|
+
json;
|
|
44428
|
+
constructor(params) {
|
|
44429
|
+
super({
|
|
44430
|
+
...params,
|
|
44431
|
+
fileContent: params.fileContent ?? ""
|
|
44432
|
+
});
|
|
44433
|
+
this.json = parseCrushConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
44434
|
+
}
|
|
44435
|
+
getJson() {
|
|
44436
|
+
return this.json;
|
|
44437
|
+
}
|
|
44438
|
+
isDeletable() {
|
|
44439
|
+
return false;
|
|
44440
|
+
}
|
|
44441
|
+
/**
|
|
44442
|
+
* `crush.json` is Crush's file: rulesync merges into it when it exists but
|
|
44443
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
44444
|
+
* absent `allowed_tools` both mean "prompt for everything".
|
|
44445
|
+
*/
|
|
44446
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
44447
|
+
return true;
|
|
44448
|
+
}
|
|
44449
|
+
static getSettablePaths({ global = false } = {}) {
|
|
44450
|
+
return getCrushConfigSettablePaths({ global });
|
|
44451
|
+
}
|
|
44452
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
44453
|
+
const location = await resolveCrushConfigFile({
|
|
44454
|
+
outputRoot,
|
|
44455
|
+
global
|
|
44456
|
+
});
|
|
44457
|
+
return new CrushPermissions({
|
|
44458
|
+
outputRoot,
|
|
44459
|
+
relativeDirPath: location.relativeDirPath,
|
|
44460
|
+
relativeFilePath: location.relativeFilePath,
|
|
44461
|
+
fileContent: crushConfigImportContent(location),
|
|
44462
|
+
validate,
|
|
44463
|
+
global
|
|
44464
|
+
});
|
|
44465
|
+
}
|
|
44466
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
44467
|
+
const location = await resolveCrushConfigFile({
|
|
44468
|
+
outputRoot,
|
|
44469
|
+
global
|
|
44470
|
+
});
|
|
44471
|
+
const existingContent = location.fileContent ?? "";
|
|
44472
|
+
warnCrushTwinLeftovers({
|
|
44473
|
+
location,
|
|
44474
|
+
ownedPaths: [[CRUSH_PERMISSIONS_KEY, CRUSH_ALLOWED_TOOLS_KEY], [CRUSH_OPTIONS_KEY, CRUSH_DISABLED_TOOLS_KEY]],
|
|
44475
|
+
logger
|
|
44476
|
+
});
|
|
44477
|
+
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
44478
|
+
const existingPermissions = isRecord$1(existing["permissions"]) ? existing[CRUSH_PERMISSIONS_KEY] : void 0;
|
|
44479
|
+
const existingOptions = isRecord$1(existing["options"]) ? existing[CRUSH_OPTIONS_KEY] : void 0;
|
|
44480
|
+
const config = rulesyncPermissions.getJson();
|
|
44481
|
+
const generated = convertRulesyncToCrushLists({
|
|
44482
|
+
config,
|
|
44483
|
+
logger
|
|
44484
|
+
});
|
|
44485
|
+
const managedToolNames = managedCrushToolNames(config);
|
|
44486
|
+
const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
44487
|
+
const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
44488
|
+
const allowedList = uniq$1([...preservedAllowed, ...generated.allowed]);
|
|
44489
|
+
const disabledList = uniq$1([...preservedDisabled, ...generated.disabled]);
|
|
44490
|
+
const patch = {};
|
|
44491
|
+
if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
|
|
44492
|
+
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
44493
|
+
return new CrushPermissions({
|
|
44494
|
+
outputRoot,
|
|
44495
|
+
relativeDirPath: location.relativeDirPath,
|
|
44496
|
+
relativeFilePath: location.relativeFilePath,
|
|
44497
|
+
fileContent: applySharedConfigPatch({
|
|
44498
|
+
fileKey: sharedConfigFileKey(CrushPermissions.getSettablePaths({ global })),
|
|
44499
|
+
feature: "permissions",
|
|
44500
|
+
existingContent,
|
|
44501
|
+
patch,
|
|
44502
|
+
filePath: location.filePath,
|
|
44503
|
+
logger
|
|
44504
|
+
}),
|
|
44505
|
+
validate: true,
|
|
44506
|
+
global
|
|
44507
|
+
});
|
|
44508
|
+
}
|
|
44509
|
+
toRulesyncPermissions() {
|
|
44510
|
+
const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
44511
|
+
const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
|
|
44512
|
+
const rulesyncConfig = convertCrushListsToRulesync({
|
|
44513
|
+
allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
|
|
44514
|
+
disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
|
|
44515
|
+
});
|
|
44516
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
44517
|
+
}
|
|
44518
|
+
validate() {
|
|
44519
|
+
return {
|
|
44520
|
+
success: true,
|
|
44521
|
+
error: null
|
|
44522
|
+
};
|
|
44523
|
+
}
|
|
44524
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
44525
|
+
return new CrushPermissions({
|
|
44526
|
+
outputRoot,
|
|
44527
|
+
relativeDirPath,
|
|
44528
|
+
relativeFilePath,
|
|
44529
|
+
fileContent: "{}",
|
|
44530
|
+
validate: false,
|
|
44531
|
+
global
|
|
44532
|
+
});
|
|
44533
|
+
}
|
|
44534
|
+
};
|
|
44535
|
+
/**
|
|
44536
|
+
* The Crush tool names the canonical config manages, i.e. the names its
|
|
44537
|
+
* categories map to. Entries naming any other tool are the user's and survive
|
|
44538
|
+
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
44539
|
+
*/
|
|
44540
|
+
function managedCrushToolNames(config) {
|
|
44541
|
+
return new Set(Object.keys(config.permission).filter((category) => category !== "*" && !isPrototypePollutionKey(category)).map((category) => toCrushToolName(category)));
|
|
44542
|
+
}
|
|
44543
|
+
/**
|
|
44544
|
+
* Reduce one category's rules to the single tool-wide action Crush can carry,
|
|
44545
|
+
* or undefined when nothing should be written for the tool: no catch-all rule,
|
|
44546
|
+
* a catch-all allow next to a narrower deny/ask (Crush cannot narrow an
|
|
44547
|
+
* allowed tool, so it keeps prompting instead), or a deny on an MCP tool
|
|
44548
|
+
* (`options.disabled_tools` only covers built-ins).
|
|
44549
|
+
*/
|
|
44550
|
+
function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
44551
|
+
let catchAllAction;
|
|
44552
|
+
let restricted = false;
|
|
44553
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
44554
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
44555
|
+
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
44556
|
+
if (pattern === CATCH_ALL_PATTERN$4) {
|
|
44557
|
+
catchAllAction = action;
|
|
44558
|
+
continue;
|
|
44559
|
+
}
|
|
44560
|
+
logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
44561
|
+
if (action !== "allow") restricted = true;
|
|
44562
|
+
}
|
|
44563
|
+
if (catchAllAction === "allow" && restricted) {
|
|
44564
|
+
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}".`);
|
|
44565
|
+
return;
|
|
44566
|
+
}
|
|
44567
|
+
if (catchAllAction === "deny" && isMcpToolName(toolName)) {
|
|
44568
|
+
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.`);
|
|
44569
|
+
return;
|
|
44570
|
+
}
|
|
44571
|
+
return catchAllAction;
|
|
44572
|
+
}
|
|
44573
|
+
/**
|
|
44574
|
+
* Convert a rulesync permissions config into Crush's two tool lists.
|
|
44575
|
+
*/
|
|
44576
|
+
function convertRulesyncToCrushLists({ config, logger }) {
|
|
44577
|
+
const lists = {
|
|
44578
|
+
allowed: [],
|
|
44579
|
+
disabled: []
|
|
44580
|
+
};
|
|
44581
|
+
const actionByTool = /* @__PURE__ */ new Map();
|
|
44582
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
44583
|
+
if (isPrototypePollutionKey(category)) continue;
|
|
44584
|
+
if (category === "*") {
|
|
44585
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
44586
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
44587
|
+
logger?.warn(`Crush has no allow-all or disable-all tool list, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
44588
|
+
}
|
|
44589
|
+
continue;
|
|
44590
|
+
}
|
|
44591
|
+
const toolName = toCrushToolName(category);
|
|
44592
|
+
const catchAllAction = resolveCategoryAction({
|
|
44593
|
+
category,
|
|
44594
|
+
toolName,
|
|
44595
|
+
rules,
|
|
44596
|
+
logger
|
|
44597
|
+
});
|
|
44598
|
+
if (catchAllAction === void 0) continue;
|
|
44599
|
+
const previous = actionByTool.get(toolName);
|
|
44600
|
+
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.`);
|
|
44601
|
+
actionByTool.set(toolName, catchAllAction);
|
|
44602
|
+
if (catchAllAction === "allow") lists.allowed.push(toolName);
|
|
44603
|
+
else if (catchAllAction === "deny") lists.disabled.push(toolName);
|
|
44604
|
+
}
|
|
44605
|
+
return {
|
|
44606
|
+
allowed: uniq$1(lists.allowed),
|
|
44607
|
+
disabled: uniq$1(lists.disabled)
|
|
44608
|
+
};
|
|
44609
|
+
}
|
|
44610
|
+
/**
|
|
44611
|
+
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
44612
|
+
* entry scopes an allow to one action of a tool, which the canonical model
|
|
44613
|
+
* cannot express, so it is left out rather than widened to the whole tool.
|
|
44614
|
+
*/
|
|
44615
|
+
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
44616
|
+
const permission = {};
|
|
44617
|
+
const bucketFor = (category) => {
|
|
44618
|
+
const own = lookupOwn({
|
|
44619
|
+
record: permission,
|
|
44620
|
+
key: category
|
|
44621
|
+
});
|
|
44622
|
+
if (own !== void 0) return own;
|
|
44623
|
+
const created = {};
|
|
44624
|
+
permission[category] = created;
|
|
44625
|
+
return created;
|
|
44626
|
+
};
|
|
44627
|
+
for (const entry of disabled) {
|
|
44628
|
+
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
44629
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
|
|
44630
|
+
}
|
|
44631
|
+
for (const entry of allowed) {
|
|
44632
|
+
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
44633
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
|
|
44634
|
+
}
|
|
44635
|
+
return { permission };
|
|
44636
|
+
}
|
|
44637
|
+
//#endregion
|
|
42519
44638
|
//#region src/features/permissions/cursor-permissions.ts
|
|
42520
44639
|
/**
|
|
42521
44640
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -43670,6 +45789,40 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43670
45789
|
return Object.keys(bash).length > 0 ? { permission: { bash } } : { permission: {} };
|
|
43671
45790
|
}
|
|
43672
45791
|
//#endregion
|
|
45792
|
+
//#region src/features/permissions/single-action-collapse.ts
|
|
45793
|
+
/**
|
|
45794
|
+
* Strictness order shared by the tools whose permission model evaluates
|
|
45795
|
+
* `deny > ask > allow`. Used wherever several canonical rules have to fold
|
|
45796
|
+
* into one entry, so the strictest action always wins.
|
|
45797
|
+
*/
|
|
45798
|
+
const PERMISSION_ACTION_PRIORITY = {
|
|
45799
|
+
allow: 0,
|
|
45800
|
+
ask: 1,
|
|
45801
|
+
deny: 2
|
|
45802
|
+
};
|
|
45803
|
+
/**
|
|
45804
|
+
* Collapse a pattern map to the single action a tool can hold for a scope
|
|
45805
|
+
* that has no pattern matcher, using deny > ask > allow precedence.
|
|
45806
|
+
*
|
|
45807
|
+
* A map without a catch-all grants nothing to unmatched inputs, so an
|
|
45808
|
+
* implicit `ask` joins the candidates and a narrow allowlist can never widen
|
|
45809
|
+
* into a blanket allow. Returns `undefined` for an empty map; each caller
|
|
45810
|
+
* decides what an empty map means for its tool (e.g. emit nothing, or fall
|
|
45811
|
+
* back to `deny` when the tool's own default is allow).
|
|
45812
|
+
*/
|
|
45813
|
+
function collapseRulesToSingleAction({ rules }) {
|
|
45814
|
+
const actions = Object.values(rules);
|
|
45815
|
+
if (actions.length === 0) return;
|
|
45816
|
+
return (Object.hasOwn(rules, "*") ? actions : [...actions, "ask"]).reduce((current, candidate) => PERMISSION_ACTION_PRIORITY[candidate] > PERMISSION_ACTION_PRIORITY[current] ? candidate : current);
|
|
45817
|
+
}
|
|
45818
|
+
/**
|
|
45819
|
+
* Whether a pattern map carries anything beyond the `*` catch-all, i.e.
|
|
45820
|
+
* whether collapsing it to a single action loses information worth a warning.
|
|
45821
|
+
*/
|
|
45822
|
+
function hasPatternSpecificRules(rules) {
|
|
45823
|
+
return Object.keys(rules).some((pattern) => pattern !== "*");
|
|
45824
|
+
}
|
|
45825
|
+
//#endregion
|
|
43673
45826
|
//#region src/features/permissions/devin-permissions.ts
|
|
43674
45827
|
/**
|
|
43675
45828
|
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
@@ -43679,17 +45832,21 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43679
45832
|
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
43680
45833
|
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
43681
45834
|
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
43682
|
-
* `edit` rules round-trip as `write` (a lossy but documented collapse).
|
|
45835
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). The
|
|
45836
|
+
* canonical `websearch` category maps onto the bare `web_search` tool name,
|
|
45837
|
+
* accepted in the permission lists since CLI v3000.10.21 (2026-09-10). Unknown
|
|
43683
45838
|
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
43684
45839
|
*
|
|
43685
45840
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
45841
|
+
* @see https://docs.devin.ai/cli/changelog/stable — v3000.10.21, `web_search`
|
|
43686
45842
|
*/
|
|
43687
45843
|
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
43688
45844
|
read: "Read",
|
|
43689
45845
|
write: "Write",
|
|
43690
45846
|
edit: "Write",
|
|
43691
45847
|
bash: "Exec",
|
|
43692
|
-
webfetch: "Fetch"
|
|
45848
|
+
webfetch: "Fetch",
|
|
45849
|
+
websearch: "web_search"
|
|
43693
45850
|
};
|
|
43694
45851
|
/**
|
|
43695
45852
|
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
@@ -43698,8 +45855,15 @@ const DEVIN_SCOPE_TO_CANONICAL = {
|
|
|
43698
45855
|
Read: "read",
|
|
43699
45856
|
Write: "write",
|
|
43700
45857
|
Exec: "bash",
|
|
43701
|
-
Fetch: "webfetch"
|
|
45858
|
+
Fetch: "webfetch",
|
|
45859
|
+
web_search: "websearch"
|
|
43702
45860
|
};
|
|
45861
|
+
/**
|
|
45862
|
+
* Devin scopes that exist only as a bare tool name: there is no
|
|
45863
|
+
* `web_search(pattern)` matcher, so a pattern-specific rule under the canonical
|
|
45864
|
+
* category cannot be expressed and is collapsed to one action instead.
|
|
45865
|
+
*/
|
|
45866
|
+
const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
|
|
43703
45867
|
function toDevinScope(canonical) {
|
|
43704
45868
|
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
43705
45869
|
}
|
|
@@ -43884,7 +46048,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43884
46048
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
43885
46049
|
}
|
|
43886
46050
|
const config = rulesyncPermissions.getJson();
|
|
43887
|
-
const { allow, ask, deny } = convertRulesyncToDevinPermissions(
|
|
46051
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions({
|
|
46052
|
+
config,
|
|
46053
|
+
logger
|
|
46054
|
+
});
|
|
43888
46055
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
43889
46056
|
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
43890
46057
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
@@ -43988,26 +46155,50 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43988
46155
|
}
|
|
43989
46156
|
};
|
|
43990
46157
|
/**
|
|
46158
|
+
* Collapse the pattern rules of a bare-only scope to the single action Devin
|
|
46159
|
+
* can hold for it, using deny > ask > allow precedence. A map without a
|
|
46160
|
+
* catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
|
|
46161
|
+
* candidates and a narrow allowlist can never widen into a blanket allow.
|
|
46162
|
+
* Returns `undefined` for an empty map: like any other empty category it
|
|
46163
|
+
* emits nothing, which leaves Devin's own auto-approve default in place.
|
|
46164
|
+
*/
|
|
46165
|
+
function collapseBareOnlyScopeRules({ category, scope, rules, logger }) {
|
|
46166
|
+
const action = collapseRulesToSingleAction({ rules });
|
|
46167
|
+
if (action === void 0) return;
|
|
46168
|
+
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.`);
|
|
46169
|
+
return action;
|
|
46170
|
+
}
|
|
46171
|
+
/**
|
|
43991
46172
|
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
43992
46173
|
*/
|
|
43993
|
-
function convertRulesyncToDevinPermissions(config) {
|
|
46174
|
+
function convertRulesyncToDevinPermissions({ config, logger }) {
|
|
43994
46175
|
const allow = [];
|
|
43995
46176
|
const ask = [];
|
|
43996
46177
|
const deny = [];
|
|
46178
|
+
const push = (entry, action) => {
|
|
46179
|
+
switch (action) {
|
|
46180
|
+
case "allow":
|
|
46181
|
+
allow.push(entry);
|
|
46182
|
+
break;
|
|
46183
|
+
case "ask":
|
|
46184
|
+
ask.push(entry);
|
|
46185
|
+
break;
|
|
46186
|
+
case "deny": deny.push(entry);
|
|
46187
|
+
}
|
|
46188
|
+
};
|
|
43997
46189
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
43998
46190
|
const scope = toDevinScope(category);
|
|
43999
|
-
|
|
44000
|
-
const
|
|
44001
|
-
|
|
44002
|
-
|
|
44003
|
-
|
|
44004
|
-
|
|
44005
|
-
|
|
44006
|
-
|
|
44007
|
-
|
|
44008
|
-
case "deny": deny.push(entry);
|
|
44009
|
-
}
|
|
46191
|
+
if (DEVIN_BARE_ONLY_SCOPES.has(scope)) {
|
|
46192
|
+
const action = collapseBareOnlyScopeRules({
|
|
46193
|
+
category,
|
|
46194
|
+
scope,
|
|
46195
|
+
rules,
|
|
46196
|
+
logger
|
|
46197
|
+
});
|
|
46198
|
+
if (action !== void 0) push(scope, action);
|
|
46199
|
+
continue;
|
|
44010
46200
|
}
|
|
46201
|
+
for (const [pattern, action] of Object.entries(rules)) push(buildDevinPermissionEntry(scope, pattern), action);
|
|
44011
46202
|
}
|
|
44012
46203
|
return {
|
|
44013
46204
|
allow,
|
|
@@ -46253,20 +48444,14 @@ const OPENCODE_ACTION_ONLY_PERMISSION_KEYS = /* @__PURE__ */ new Set([
|
|
|
46253
48444
|
"question",
|
|
46254
48445
|
"doom_loop"
|
|
46255
48446
|
]);
|
|
46256
|
-
const PERMISSION_ACTION_PRIORITY = {
|
|
46257
|
-
allow: 0,
|
|
46258
|
-
ask: 1,
|
|
46259
|
-
deny: 2
|
|
46260
|
-
};
|
|
46261
48447
|
function toOpencodePermission({ category, value, logger }) {
|
|
46262
48448
|
if (typeof value === "string" || !OPENCODE_ACTION_ONLY_PERMISSION_KEYS.has(category)) return value;
|
|
46263
|
-
const
|
|
46264
|
-
if (
|
|
48449
|
+
const action = collapseRulesToSingleAction({ rules: value });
|
|
48450
|
+
if (action === void 0) {
|
|
46265
48451
|
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.`);
|
|
46266
48452
|
return "deny";
|
|
46267
48453
|
}
|
|
46268
|
-
|
|
46269
|
-
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.`);
|
|
48454
|
+
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.`);
|
|
46270
48455
|
return action;
|
|
46271
48456
|
}
|
|
46272
48457
|
/**
|
|
@@ -46644,6 +48829,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
46644
48829
|
"visible",
|
|
46645
48830
|
"eager",
|
|
46646
48831
|
"listDirectory",
|
|
48832
|
+
"todoWrite",
|
|
46647
48833
|
"workflowsEnabled"
|
|
46648
48834
|
];
|
|
46649
48835
|
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
@@ -46749,6 +48935,11 @@ const QWEN_OVERRIDE_GROUPS = [{
|
|
|
46749
48935
|
rule: "global-machine-wide",
|
|
46750
48936
|
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.`,
|
|
46751
48937
|
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."
|
|
48938
|
+
},
|
|
48939
|
+
todoWrite: {
|
|
48940
|
+
rule: "global-machine-wide",
|
|
48941
|
+
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.`,
|
|
48942
|
+
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."
|
|
46752
48943
|
}
|
|
46753
48944
|
}
|
|
46754
48945
|
}, {
|
|
@@ -48555,9 +50746,25 @@ function widenToPrefixGlob(pattern) {
|
|
|
48555
50746
|
function toShellEntry(prefix) {
|
|
48556
50747
|
return prefix === "" ? SHELL_TOOL_NAME : `${SHELL_TOOL_NAME}(${prefix})`;
|
|
48557
50748
|
}
|
|
48558
|
-
/**
|
|
50749
|
+
/**
|
|
50750
|
+
* The `bash` pattern a `run_shell_command(<prefix>)` entry stands for. A
|
|
50751
|
+
* prefix Tabnine matches literally, so one an author spelled glob-like
|
|
50752
|
+
* (`git *`, `*`) is read as the glob it looks like rather than doubled into
|
|
50753
|
+
* `git * *`, which no comparison or warning could make sense of.
|
|
50754
|
+
*/
|
|
48559
50755
|
function toShellPattern(prefix) {
|
|
48560
|
-
|
|
50756
|
+
if (prefix === void 0 || prefix === "" || prefix === "*") return "*";
|
|
50757
|
+
return prefix.endsWith(" *") ? prefix : `${prefix} *`;
|
|
50758
|
+
}
|
|
50759
|
+
/**
|
|
50760
|
+
* Whether {@link toShellPattern} read the prefix as a glob: such an entry
|
|
50761
|
+
* would never fire on Tabnine as spelled (the prefix is matched literally, so
|
|
50762
|
+
* `run_shell_command(git *)` only matches a command line starting with the
|
|
50763
|
+
* two characters `git *`), and rulesync writes it back as the prefix the glob
|
|
50764
|
+
* denotes — a wider entry that the author is told about.
|
|
50765
|
+
*/
|
|
50766
|
+
function isGlobSpelledPrefix(prefix) {
|
|
50767
|
+
return prefix !== void 0 && (prefix === "*" || prefix.endsWith(" *"));
|
|
48561
50768
|
}
|
|
48562
50769
|
/**
|
|
48563
50770
|
* Split a `tools.allowed`/`tools.exclude` entry into its tool name and the
|
|
@@ -48683,7 +50890,7 @@ function collectTrustAffectingOverrideEntries({ tools, general }) {
|
|
|
48683
50890
|
const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
|
|
48684
50891
|
if (allowed.length > 0) entries.push({
|
|
48685
50892
|
label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
|
|
48686
|
-
reason: "auto-approves what it names as the override spells it; a non-shell entry
|
|
50893
|
+
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"
|
|
48687
50894
|
});
|
|
48688
50895
|
if (Object.hasOwn(tools, SANDBOX_KEY) && isNotTrue(tools[SANDBOX_KEY])) entries.push({
|
|
48689
50896
|
label: `tools.sandbox = ${quoteValueForWarning(tools[SANDBOX_KEY])}`,
|
|
@@ -48794,6 +51001,54 @@ function buildToolLists({ permission, overrideShellAllowPatterns, logger }) {
|
|
|
48794
51001
|
};
|
|
48795
51002
|
}
|
|
48796
51003
|
/**
|
|
51004
|
+
* Names the glob-spelled `run_shell_command(...)` override entries that were
|
|
51005
|
+
* written as the prefix their glob denotes. Said only for the entries that
|
|
51006
|
+
* made it into the list: one that a deny or ask overlaps is announced as
|
|
51007
|
+
* withheld by the comparison instead, and one whose glob names no prefix at
|
|
51008
|
+
* all is skipped by it.
|
|
51009
|
+
*/
|
|
51010
|
+
function warnAboutGlobSpelledOverrideEntries({ entries, allowed, logger }) {
|
|
51011
|
+
for (const { entry, pattern } of entries) {
|
|
51012
|
+
const prefix = toShellPrefix(pattern);
|
|
51013
|
+
const written = prefix === void 0 ? void 0 : toShellEntry(prefix);
|
|
51014
|
+
if (written === void 0 || !allowed.includes(written)) continue;
|
|
51015
|
+
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.`);
|
|
51016
|
+
}
|
|
51017
|
+
}
|
|
51018
|
+
/**
|
|
51019
|
+
* A verbatim override allow naming a tool the canonical block denies is
|
|
51020
|
+
* withheld the way a shadowed `bash` allow is: Tabnine never loads an excluded
|
|
51021
|
+
* tool whatever `allowed` says, so the entry would only ever contradict the
|
|
51022
|
+
* deny it sits beside — and an override is not a way around a canonical rule.
|
|
51023
|
+
* Returns the entries that may be written.
|
|
51024
|
+
*/
|
|
51025
|
+
function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logger }) {
|
|
51026
|
+
const denied = overrideVerbatimAllowed.filter((entry) => {
|
|
51027
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
51028
|
+
return toolName !== void 0 && exclude.includes(toolName);
|
|
51029
|
+
});
|
|
51030
|
+
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.`);
|
|
51031
|
+
return overrideVerbatimAllowed.filter((entry) => !denied.includes(entry));
|
|
51032
|
+
}
|
|
51033
|
+
/**
|
|
51034
|
+
* The entries of a managed tool that the canonical block did not re-derive are
|
|
51035
|
+
* named rather than removed silently. Dropping an exclude loosens the policy,
|
|
51036
|
+
* so it is a warning; dropping an allow only brings back Tabnine's confirmation
|
|
51037
|
+
* prompt, so it is said at info level for the author who wonders where a
|
|
51038
|
+
* hand-written entry went.
|
|
51039
|
+
*/
|
|
51040
|
+
function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
|
|
51041
|
+
const dropped = (key, list) => uniq(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
|
|
51042
|
+
if (list.includes(entry)) return false;
|
|
51043
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
51044
|
+
return toolName === void 0 || !list.includes(toolName);
|
|
51045
|
+
});
|
|
51046
|
+
const droppedExclude = dropped(EXCLUDE_KEY, excludeList);
|
|
51047
|
+
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.`);
|
|
51048
|
+
const droppedAllowed = dropped(ALLOWED_KEY, allowedList);
|
|
51049
|
+
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.`);
|
|
51050
|
+
}
|
|
51051
|
+
/**
|
|
48797
51052
|
* Permissions generator for the Tabnine CLI.
|
|
48798
51053
|
*
|
|
48799
51054
|
* Tabnine CLI keeps its tool policy in `.tabnine/agent/settings.json` (project)
|
|
@@ -48859,7 +51114,10 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48859
51114
|
const config = rulesyncPermissions.getJson();
|
|
48860
51115
|
const override = isRecord$1(config.tabnine) ? config.tabnine : {};
|
|
48861
51116
|
const { tools: overrideTools, general: overrideGeneral, refused: refusedPaths } = stripRefusedOverridePaths(override);
|
|
48862
|
-
if (refusedPaths.length > 0)
|
|
51117
|
+
if (refusedPaths.length > 0) {
|
|
51118
|
+
const one = refusedPaths.length === 1;
|
|
51119
|
+
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 ${join(paths.relativeDirPath, paths.relativeFilePath)} if you need ${one ? "it" : "them"}.`);
|
|
51120
|
+
}
|
|
48863
51121
|
const overrideList = (key) => {
|
|
48864
51122
|
const value = overrideTools[key];
|
|
48865
51123
|
if (value === void 0 || isStringArray$2(value)) return value ?? [];
|
|
@@ -48868,10 +51126,17 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48868
51126
|
};
|
|
48869
51127
|
const overrideShellAllowPatterns = [];
|
|
48870
51128
|
const overrideVerbatimAllowed = [];
|
|
51129
|
+
const globSpelledOverrideEntries = [];
|
|
48871
51130
|
for (const entry of overrideList(ALLOWED_KEY)) {
|
|
48872
51131
|
const parsed = parseTabnineEntry(entry);
|
|
48873
|
-
if (parsed?.toolName === SHELL_TOOL_NAME)
|
|
48874
|
-
|
|
51132
|
+
if (parsed?.toolName === SHELL_TOOL_NAME) {
|
|
51133
|
+
const pattern = toShellPattern(parsed.prefix);
|
|
51134
|
+
if (isGlobSpelledPrefix(parsed.prefix)) globSpelledOverrideEntries.push({
|
|
51135
|
+
entry,
|
|
51136
|
+
pattern
|
|
51137
|
+
});
|
|
51138
|
+
overrideShellAllowPatterns.push(pattern);
|
|
51139
|
+
} else overrideVerbatimAllowed.push(entry);
|
|
48875
51140
|
}
|
|
48876
51141
|
const overrideExclude = overrideList(EXCLUDE_KEY);
|
|
48877
51142
|
const { allowed, exclude } = buildToolLists({
|
|
@@ -48879,6 +51144,16 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48879
51144
|
overrideShellAllowPatterns,
|
|
48880
51145
|
logger
|
|
48881
51146
|
});
|
|
51147
|
+
warnAboutGlobSpelledOverrideEntries({
|
|
51148
|
+
entries: globSpelledOverrideEntries,
|
|
51149
|
+
allowed,
|
|
51150
|
+
logger
|
|
51151
|
+
});
|
|
51152
|
+
const writableOverrideAllowed = withholdDeniedOverrideAllowed({
|
|
51153
|
+
overrideVerbatimAllowed,
|
|
51154
|
+
exclude,
|
|
51155
|
+
logger
|
|
51156
|
+
});
|
|
48882
51157
|
const ignoredOverrideKeys = Object.keys(override).filter((key) => key !== TOOLS_KEY && key !== GENERAL_KEY);
|
|
48883
51158
|
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.`);
|
|
48884
51159
|
const existingTools = this.existingToolsGroup(existingContent);
|
|
@@ -48889,7 +51164,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48889
51164
|
});
|
|
48890
51165
|
const allowedList = uniq([
|
|
48891
51166
|
...allowed,
|
|
48892
|
-
...
|
|
51167
|
+
...writableOverrideAllowed,
|
|
48893
51168
|
...preservedEntries(ALLOWED_KEY)
|
|
48894
51169
|
]);
|
|
48895
51170
|
const excludeList = uniq([
|
|
@@ -48897,12 +51172,12 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48897
51172
|
...overrideExclude,
|
|
48898
51173
|
...preservedEntries(EXCLUDE_KEY)
|
|
48899
51174
|
]);
|
|
48900
|
-
|
|
48901
|
-
|
|
48902
|
-
|
|
48903
|
-
|
|
51175
|
+
reportDroppedManagedEntries({
|
|
51176
|
+
existingTools,
|
|
51177
|
+
allowedList,
|
|
51178
|
+
excludeList,
|
|
51179
|
+
logger
|
|
48904
51180
|
});
|
|
48905
|
-
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.`);
|
|
48906
51181
|
const tools = {
|
|
48907
51182
|
...overrideTools,
|
|
48908
51183
|
[ALLOWED_KEY]: allowedList.length > 0 ? allowedList : void 0,
|
|
@@ -48916,7 +51191,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48916
51191
|
entries: collectTrustAffectingOverrideEntries({
|
|
48917
51192
|
tools: {
|
|
48918
51193
|
...overrideTools,
|
|
48919
|
-
[ALLOWED_KEY]:
|
|
51194
|
+
[ALLOWED_KEY]: writableOverrideAllowed
|
|
48920
51195
|
},
|
|
48921
51196
|
general: overrideGeneral
|
|
48922
51197
|
}),
|
|
@@ -52249,6 +54524,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52249
54524
|
supportsImport: true
|
|
52250
54525
|
}
|
|
52251
54526
|
}],
|
|
54527
|
+
["commandcode", {
|
|
54528
|
+
class: CommandcodePermissions,
|
|
54529
|
+
meta: {
|
|
54530
|
+
supportsProject: true,
|
|
54531
|
+
supportsGlobal: true,
|
|
54532
|
+
supportsImport: true
|
|
54533
|
+
}
|
|
54534
|
+
}],
|
|
52252
54535
|
["continue", {
|
|
52253
54536
|
class: ContinuePermissions,
|
|
52254
54537
|
meta: {
|
|
@@ -52273,6 +54556,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52273
54556
|
supportsImport: true
|
|
52274
54557
|
}
|
|
52275
54558
|
}],
|
|
54559
|
+
["crush", {
|
|
54560
|
+
class: CrushPermissions,
|
|
54561
|
+
meta: {
|
|
54562
|
+
supportsProject: true,
|
|
54563
|
+
supportsGlobal: true,
|
|
54564
|
+
supportsImport: true
|
|
54565
|
+
}
|
|
54566
|
+
}],
|
|
52276
54567
|
["cursor", {
|
|
52277
54568
|
class: CursorPermissions,
|
|
52278
54569
|
meta: {
|
|
@@ -55007,6 +57298,154 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
55007
57298
|
}
|
|
55008
57299
|
};
|
|
55009
57300
|
//#endregion
|
|
57301
|
+
//#region src/features/skills/commandcode-skill.ts
|
|
57302
|
+
const CommandcodeSkillFrontmatterSchema = z.looseObject({
|
|
57303
|
+
name: z.string(),
|
|
57304
|
+
description: z.string()
|
|
57305
|
+
});
|
|
57306
|
+
/**
|
|
57307
|
+
* Represents a Command Code skill directory.
|
|
57308
|
+
*
|
|
57309
|
+
* Command Code discovers Anthropic-style `<name>/SKILL.md` directories under
|
|
57310
|
+
* `<project>/.commandcode/skills/` (project scope) and
|
|
57311
|
+
* `~/.commandcode/skills/` (user scope). The frontmatter requires `name`
|
|
57312
|
+
* (matching the directory name) and `description`; supporting files next to
|
|
57313
|
+
* `SKILL.md` are carried along.
|
|
57314
|
+
*
|
|
57315
|
+
* @see https://commandcode.ai/docs/skills
|
|
57316
|
+
*/
|
|
57317
|
+
var CommandcodeSkill = class CommandcodeSkill extends ToolSkill {
|
|
57318
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = COMMANDCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
57319
|
+
super({
|
|
57320
|
+
outputRoot,
|
|
57321
|
+
relativeDirPath,
|
|
57322
|
+
dirName,
|
|
57323
|
+
mainFile: {
|
|
57324
|
+
name: SKILL_FILE_NAME,
|
|
57325
|
+
body,
|
|
57326
|
+
frontmatter: { ...frontmatter }
|
|
57327
|
+
},
|
|
57328
|
+
otherFiles,
|
|
57329
|
+
global
|
|
57330
|
+
});
|
|
57331
|
+
if (validate) {
|
|
57332
|
+
const result = this.validate();
|
|
57333
|
+
if (!result.success) throw result.error;
|
|
57334
|
+
}
|
|
57335
|
+
}
|
|
57336
|
+
static getSettablePaths(_options = {}) {
|
|
57337
|
+
return { relativeDirPath: COMMANDCODE_SKILLS_DIR_PATH };
|
|
57338
|
+
}
|
|
57339
|
+
getFrontmatter() {
|
|
57340
|
+
return CommandcodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
57341
|
+
}
|
|
57342
|
+
getBody() {
|
|
57343
|
+
return this.mainFile?.body ?? "";
|
|
57344
|
+
}
|
|
57345
|
+
validate() {
|
|
57346
|
+
if (!this.mainFile) return {
|
|
57347
|
+
success: false,
|
|
57348
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
57349
|
+
};
|
|
57350
|
+
const result = CommandcodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
57351
|
+
if (!result.success) return {
|
|
57352
|
+
success: false,
|
|
57353
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
57354
|
+
};
|
|
57355
|
+
if (result.data.name !== this.getDirName()) return {
|
|
57356
|
+
success: false,
|
|
57357
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: frontmatter name (${result.data.name}) must match directory name (${this.getDirName()})`)
|
|
57358
|
+
};
|
|
57359
|
+
return {
|
|
57360
|
+
success: true,
|
|
57361
|
+
error: null
|
|
57362
|
+
};
|
|
57363
|
+
}
|
|
57364
|
+
toRulesyncSkill() {
|
|
57365
|
+
const { name, description, ...commandcodeSection } = this.getFrontmatter();
|
|
57366
|
+
const rulesyncFrontmatter = {
|
|
57367
|
+
name,
|
|
57368
|
+
description,
|
|
57369
|
+
targets: ["*"],
|
|
57370
|
+
...Object.keys(commandcodeSection).length > 0 && { commandcode: commandcodeSection }
|
|
57371
|
+
};
|
|
57372
|
+
return new RulesyncSkill({
|
|
57373
|
+
outputRoot: this.outputRoot,
|
|
57374
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
57375
|
+
dirName: this.getDirName(),
|
|
57376
|
+
frontmatter: rulesyncFrontmatter,
|
|
57377
|
+
body: this.getBody(),
|
|
57378
|
+
otherFiles: this.getOtherFiles(),
|
|
57379
|
+
validate: true,
|
|
57380
|
+
global: this.global
|
|
57381
|
+
});
|
|
57382
|
+
}
|
|
57383
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
57384
|
+
const settablePaths = CommandcodeSkill.getSettablePaths({ global });
|
|
57385
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
57386
|
+
const { name: _sectionName, description: _sectionDescription, ...commandcodeSection } = rulesyncFrontmatter.commandcode ?? {};
|
|
57387
|
+
const commandcodeFrontmatter = {
|
|
57388
|
+
...commandcodeSection,
|
|
57389
|
+
name: rulesyncFrontmatter.name,
|
|
57390
|
+
description: rulesyncFrontmatter.description
|
|
57391
|
+
};
|
|
57392
|
+
return new CommandcodeSkill({
|
|
57393
|
+
outputRoot,
|
|
57394
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
57395
|
+
dirName: commandcodeFrontmatter.name,
|
|
57396
|
+
frontmatter: commandcodeFrontmatter,
|
|
57397
|
+
body: rulesyncSkill.getBody(),
|
|
57398
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
57399
|
+
validate,
|
|
57400
|
+
global
|
|
57401
|
+
});
|
|
57402
|
+
}
|
|
57403
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
57404
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
57405
|
+
return targets.includes("*") || targets.includes("commandcode");
|
|
57406
|
+
}
|
|
57407
|
+
static async fromDir(params) {
|
|
57408
|
+
const loaded = await this.loadSkillDirContent({
|
|
57409
|
+
...params,
|
|
57410
|
+
getSettablePaths: CommandcodeSkill.getSettablePaths
|
|
57411
|
+
});
|
|
57412
|
+
const result = CommandcodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
57413
|
+
if (!result.success) {
|
|
57414
|
+
const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
57415
|
+
throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
57416
|
+
}
|
|
57417
|
+
if (result.data.name !== loaded.dirName) {
|
|
57418
|
+
const skillFilePath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName, SKILL_FILE_NAME);
|
|
57419
|
+
throw new Error(`Frontmatter name (${result.data.name}) must match directory name (${loaded.dirName}) in ${skillFilePath}`);
|
|
57420
|
+
}
|
|
57421
|
+
return new CommandcodeSkill({
|
|
57422
|
+
outputRoot: loaded.outputRoot,
|
|
57423
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
57424
|
+
dirName: loaded.dirName,
|
|
57425
|
+
frontmatter: result.data,
|
|
57426
|
+
body: loaded.body,
|
|
57427
|
+
otherFiles: loaded.otherFiles,
|
|
57428
|
+
validate: true,
|
|
57429
|
+
global: loaded.global
|
|
57430
|
+
});
|
|
57431
|
+
}
|
|
57432
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
57433
|
+
return new CommandcodeSkill({
|
|
57434
|
+
outputRoot,
|
|
57435
|
+
relativeDirPath,
|
|
57436
|
+
dirName,
|
|
57437
|
+
frontmatter: {
|
|
57438
|
+
name: "",
|
|
57439
|
+
description: ""
|
|
57440
|
+
},
|
|
57441
|
+
body: "",
|
|
57442
|
+
otherFiles: [],
|
|
57443
|
+
validate: false,
|
|
57444
|
+
global
|
|
57445
|
+
});
|
|
57446
|
+
}
|
|
57447
|
+
};
|
|
57448
|
+
//#endregion
|
|
55010
57449
|
//#region src/features/skills/continue-skill.ts
|
|
55011
57450
|
const ContinueSkillFrontmatterSchema = z.looseObject({
|
|
55012
57451
|
name: z.string(),
|
|
@@ -58294,6 +60733,183 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
58294
60733
|
}
|
|
58295
60734
|
};
|
|
58296
60735
|
//#endregion
|
|
60736
|
+
//#region src/constants/pool-paths.ts
|
|
60737
|
+
/**
|
|
60738
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
60739
|
+
*
|
|
60740
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
60741
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
60742
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
60743
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
60744
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
60745
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
60746
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
60747
|
+
*
|
|
60748
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
60749
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
60750
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
60751
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
60752
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
60753
|
+
*
|
|
60754
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
60755
|
+
* @see https://docs.poolside.ai/skills
|
|
60756
|
+
* @see https://github.com/poolsideai/pool
|
|
60757
|
+
*/
|
|
60758
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
60759
|
+
const POOL_DIR = ".poolside";
|
|
60760
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
60761
|
+
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
60762
|
+
/** Project skills root, relative to the project root. */
|
|
60763
|
+
const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
|
|
60764
|
+
/** Global skills root, relative to the home directory. */
|
|
60765
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
|
|
60766
|
+
//#endregion
|
|
60767
|
+
//#region src/features/skills/pool-skill.ts
|
|
60768
|
+
const PoolSkillFrontmatterSchema = z.looseObject({
|
|
60769
|
+
name: z.string(),
|
|
60770
|
+
description: z.string()
|
|
60771
|
+
});
|
|
60772
|
+
/**
|
|
60773
|
+
* Represents a Pool (Poolside) skill directory.
|
|
60774
|
+
*
|
|
60775
|
+
* Pool discovers Agent Skills (`<name>/SKILL.md` bundles, optionally with
|
|
60776
|
+
* supporting files) from `.poolside/skills/` (project) and
|
|
60777
|
+
* `~/.config/poolside/skills/` (global). It also scans the shared
|
|
60778
|
+
* `.agents/skills/` / `~/.agents/skills/` roots and the skill directories of
|
|
60779
|
+
* other Agent Skills tools; those belong to their own targets, so this class
|
|
60780
|
+
* writes only the two Pool-specific roots and a skill is written exactly once.
|
|
60781
|
+
* Pool requires the directory name to equal the frontmatter `name`, otherwise
|
|
60782
|
+
* it skips the skill. Unlike adapters that reject such a skill outright, this
|
|
60783
|
+
* one warns and still writes it: the directory name is the canonical identity
|
|
60784
|
+
* shared with every other target, and Pool merely skips the skill rather than
|
|
60785
|
+
* failing, so a hard error here would block the other targets for a
|
|
60786
|
+
* Pool-only concern. Import is lenient for the same reason.
|
|
60787
|
+
* @see https://docs.poolside.ai/skills
|
|
60788
|
+
*/
|
|
60789
|
+
var PoolSkill = class PoolSkill extends ToolSkill {
|
|
60790
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = POOL_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
60791
|
+
super({
|
|
60792
|
+
outputRoot,
|
|
60793
|
+
relativeDirPath,
|
|
60794
|
+
dirName,
|
|
60795
|
+
mainFile: {
|
|
60796
|
+
name: SKILL_FILE_NAME,
|
|
60797
|
+
body,
|
|
60798
|
+
frontmatter: { ...frontmatter }
|
|
60799
|
+
},
|
|
60800
|
+
otherFiles,
|
|
60801
|
+
global
|
|
60802
|
+
});
|
|
60803
|
+
if (validate) {
|
|
60804
|
+
const result = this.validate();
|
|
60805
|
+
if (!result.success) throw result.error;
|
|
60806
|
+
}
|
|
60807
|
+
}
|
|
60808
|
+
static getSettablePaths({ global = false } = {}) {
|
|
60809
|
+
return { relativeDirPath: global ? POOL_GLOBAL_SKILLS_DIR_PATH : POOL_SKILLS_DIR_PATH };
|
|
60810
|
+
}
|
|
60811
|
+
getFrontmatter() {
|
|
60812
|
+
return PoolSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
60813
|
+
}
|
|
60814
|
+
getBody() {
|
|
60815
|
+
return this.mainFile?.body ?? "";
|
|
60816
|
+
}
|
|
60817
|
+
validate() {
|
|
60818
|
+
if (!this.mainFile) return {
|
|
60819
|
+
success: false,
|
|
60820
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
60821
|
+
};
|
|
60822
|
+
const result = PoolSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
60823
|
+
if (!result.success) return {
|
|
60824
|
+
success: false,
|
|
60825
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
60826
|
+
};
|
|
60827
|
+
return {
|
|
60828
|
+
success: true,
|
|
60829
|
+
error: null
|
|
60830
|
+
};
|
|
60831
|
+
}
|
|
60832
|
+
toRulesyncSkill() {
|
|
60833
|
+
const frontmatter = this.getFrontmatter();
|
|
60834
|
+
const rulesyncFrontmatter = {
|
|
60835
|
+
name: frontmatter.name,
|
|
60836
|
+
description: frontmatter.description,
|
|
60837
|
+
targets: ["*"]
|
|
60838
|
+
};
|
|
60839
|
+
return new RulesyncSkill({
|
|
60840
|
+
outputRoot: this.outputRoot,
|
|
60841
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
60842
|
+
dirName: this.getDirName(),
|
|
60843
|
+
frontmatter: rulesyncFrontmatter,
|
|
60844
|
+
body: this.getBody(),
|
|
60845
|
+
otherFiles: this.getOtherFiles(),
|
|
60846
|
+
validate: true,
|
|
60847
|
+
global: this.global
|
|
60848
|
+
});
|
|
60849
|
+
}
|
|
60850
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
60851
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
60852
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
60853
|
+
const dirName = rulesyncSkill.getDirName();
|
|
60854
|
+
const poolFrontmatter = {
|
|
60855
|
+
name: rulesyncFrontmatter.name,
|
|
60856
|
+
description: rulesyncFrontmatter.description
|
|
60857
|
+
};
|
|
60858
|
+
if (poolFrontmatter.name !== dirName) warnWithFallback(logger, `${stripControlCharacters(toPosixPath(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\``);
|
|
60859
|
+
return new PoolSkill({
|
|
60860
|
+
outputRoot,
|
|
60861
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
60862
|
+
dirName,
|
|
60863
|
+
frontmatter: poolFrontmatter,
|
|
60864
|
+
body: rulesyncSkill.getBody(),
|
|
60865
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
60866
|
+
validate,
|
|
60867
|
+
global
|
|
60868
|
+
});
|
|
60869
|
+
}
|
|
60870
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
60871
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
60872
|
+
return targets.includes("*") || targets.includes("pool");
|
|
60873
|
+
}
|
|
60874
|
+
static async fromDir(params) {
|
|
60875
|
+
const loaded = await this.loadSkillDirContent({
|
|
60876
|
+
...params,
|
|
60877
|
+
getSettablePaths: PoolSkill.getSettablePaths
|
|
60878
|
+
});
|
|
60879
|
+
const result = PoolSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
60880
|
+
if (!result.success) {
|
|
60881
|
+
const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
60882
|
+
throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
60883
|
+
}
|
|
60884
|
+
return new PoolSkill({
|
|
60885
|
+
outputRoot: loaded.outputRoot,
|
|
60886
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
60887
|
+
dirName: loaded.dirName,
|
|
60888
|
+
frontmatter: result.data,
|
|
60889
|
+
body: loaded.body,
|
|
60890
|
+
otherFiles: loaded.otherFiles,
|
|
60891
|
+
validate: true,
|
|
60892
|
+
global: loaded.global
|
|
60893
|
+
});
|
|
60894
|
+
}
|
|
60895
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
60896
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
60897
|
+
return new PoolSkill({
|
|
60898
|
+
outputRoot,
|
|
60899
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
60900
|
+
dirName,
|
|
60901
|
+
frontmatter: {
|
|
60902
|
+
name: "",
|
|
60903
|
+
description: ""
|
|
60904
|
+
},
|
|
60905
|
+
body: "",
|
|
60906
|
+
otherFiles: [],
|
|
60907
|
+
validate: false,
|
|
60908
|
+
global
|
|
60909
|
+
});
|
|
60910
|
+
}
|
|
60911
|
+
};
|
|
60912
|
+
//#endregion
|
|
58297
60913
|
//#region src/features/skills/qwencode-skill.ts
|
|
58298
60914
|
const QwencodeSkillFrontmatterSchema = z.looseObject({
|
|
58299
60915
|
name: z.string(),
|
|
@@ -60019,6 +62635,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60019
62635
|
supportsGlobal: true
|
|
60020
62636
|
}
|
|
60021
62637
|
}],
|
|
62638
|
+
["commandcode", {
|
|
62639
|
+
class: CommandcodeSkill,
|
|
62640
|
+
meta: {
|
|
62641
|
+
supportsProject: true,
|
|
62642
|
+
supportsSimulated: false,
|
|
62643
|
+
supportsGlobal: true
|
|
62644
|
+
}
|
|
62645
|
+
}],
|
|
60022
62646
|
["continue", {
|
|
60023
62647
|
class: ContinueSkill,
|
|
60024
62648
|
meta: {
|
|
@@ -60180,6 +62804,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60180
62804
|
supportsGlobal: true
|
|
60181
62805
|
}
|
|
60182
62806
|
}],
|
|
62807
|
+
["pool", {
|
|
62808
|
+
class: PoolSkill,
|
|
62809
|
+
meta: {
|
|
62810
|
+
supportsProject: true,
|
|
62811
|
+
supportsSimulated: false,
|
|
62812
|
+
supportsGlobal: true
|
|
62813
|
+
}
|
|
62814
|
+
}],
|
|
60183
62815
|
["qwencode", {
|
|
60184
62816
|
class: QwencodeSkill,
|
|
60185
62817
|
meta: {
|
|
@@ -62114,6 +64746,143 @@ var CodexCliSubagent = class CodexCliSubagent extends ToolSubagent {
|
|
|
62114
64746
|
}
|
|
62115
64747
|
};
|
|
62116
64748
|
//#endregion
|
|
64749
|
+
//#region src/features/subagents/commandcode-subagent.ts
|
|
64750
|
+
const CommandcodeSubagentFrontmatterSchema = z.looseObject({
|
|
64751
|
+
name: z.string(),
|
|
64752
|
+
description: z.optional(z.string()),
|
|
64753
|
+
tools: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
64754
|
+
disallowedTools: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
64755
|
+
model: z.optional(z.string()),
|
|
64756
|
+
reasoningEffort: z.optional(z.string()),
|
|
64757
|
+
maxTurns: z.optional(z.number()),
|
|
64758
|
+
permissionMode: z.optional(z.string()),
|
|
64759
|
+
background: z.optional(z.boolean()),
|
|
64760
|
+
showOutput: z.optional(z.boolean())
|
|
64761
|
+
});
|
|
64762
|
+
/**
|
|
64763
|
+
* Command Code custom agent: a Markdown file with YAML frontmatter under
|
|
64764
|
+
* `.commandcode/agents/` (project) or `~/.commandcode/agents/` (user); the
|
|
64765
|
+
* body is the agent's system prompt.
|
|
64766
|
+
*
|
|
64767
|
+
* @see https://commandcode.ai/docs/custom-agents
|
|
64768
|
+
*/
|
|
64769
|
+
var CommandcodeSubagent = class CommandcodeSubagent extends ToolSubagent {
|
|
64770
|
+
frontmatter;
|
|
64771
|
+
body;
|
|
64772
|
+
constructor({ frontmatter, body, fileContent, ...rest }) {
|
|
64773
|
+
if (rest.validate !== false) {
|
|
64774
|
+
const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
64775
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
64776
|
+
}
|
|
64777
|
+
super({
|
|
64778
|
+
...rest,
|
|
64779
|
+
fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
|
|
64780
|
+
});
|
|
64781
|
+
this.frontmatter = frontmatter;
|
|
64782
|
+
this.body = body;
|
|
64783
|
+
}
|
|
64784
|
+
static getSettablePaths(_options = {}) {
|
|
64785
|
+
return { relativeDirPath: COMMANDCODE_AGENTS_DIR_PATH };
|
|
64786
|
+
}
|
|
64787
|
+
getFrontmatter() {
|
|
64788
|
+
return this.frontmatter;
|
|
64789
|
+
}
|
|
64790
|
+
getBody() {
|
|
64791
|
+
return this.body;
|
|
64792
|
+
}
|
|
64793
|
+
toRulesyncSubagent() {
|
|
64794
|
+
const { name, description, ...rest } = this.frontmatter;
|
|
64795
|
+
return new RulesyncSubagent({
|
|
64796
|
+
outputRoot: ".",
|
|
64797
|
+
frontmatter: {
|
|
64798
|
+
targets: ["*"],
|
|
64799
|
+
name,
|
|
64800
|
+
description,
|
|
64801
|
+
commandcode: { ...rest }
|
|
64802
|
+
},
|
|
64803
|
+
body: this.body,
|
|
64804
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
64805
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
64806
|
+
validate: true
|
|
64807
|
+
});
|
|
64808
|
+
}
|
|
64809
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
64810
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
64811
|
+
const commandcodeSection = rulesyncFrontmatter.commandcode ?? {};
|
|
64812
|
+
const commandcodeSubagentFrontmatter = {
|
|
64813
|
+
name: rulesyncFrontmatter.name,
|
|
64814
|
+
description: rulesyncFrontmatter.description,
|
|
64815
|
+
...commandcodeSection
|
|
64816
|
+
};
|
|
64817
|
+
const body = rulesyncSubagent.getBody();
|
|
64818
|
+
const fileContent = stringifyFrontmatter(body, commandcodeSubagentFrontmatter, { avoidBlockScalars: true });
|
|
64819
|
+
const paths = this.getSettablePaths({ global });
|
|
64820
|
+
return new CommandcodeSubagent({
|
|
64821
|
+
outputRoot,
|
|
64822
|
+
frontmatter: commandcodeSubagentFrontmatter,
|
|
64823
|
+
body,
|
|
64824
|
+
relativeDirPath: paths.relativeDirPath,
|
|
64825
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
64826
|
+
fileContent,
|
|
64827
|
+
validate,
|
|
64828
|
+
global
|
|
64829
|
+
});
|
|
64830
|
+
}
|
|
64831
|
+
validate() {
|
|
64832
|
+
if (!this.frontmatter) return {
|
|
64833
|
+
success: true,
|
|
64834
|
+
error: null
|
|
64835
|
+
};
|
|
64836
|
+
const result = CommandcodeSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
64837
|
+
if (result.success) return {
|
|
64838
|
+
success: true,
|
|
64839
|
+
error: null
|
|
64840
|
+
};
|
|
64841
|
+
else return {
|
|
64842
|
+
success: false,
|
|
64843
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
64844
|
+
};
|
|
64845
|
+
}
|
|
64846
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
64847
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
64848
|
+
rulesyncSubagent,
|
|
64849
|
+
toolTarget: "commandcode"
|
|
64850
|
+
});
|
|
64851
|
+
}
|
|
64852
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
64853
|
+
const paths = this.getSettablePaths({ global });
|
|
64854
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
64855
|
+
const fileContent = await readFileContent(filePath);
|
|
64856
|
+
const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
|
|
64857
|
+
const result = CommandcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
64858
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
64859
|
+
return new CommandcodeSubagent({
|
|
64860
|
+
outputRoot,
|
|
64861
|
+
relativeDirPath: paths.relativeDirPath,
|
|
64862
|
+
relativeFilePath,
|
|
64863
|
+
frontmatter: result.data,
|
|
64864
|
+
body: content.trim(),
|
|
64865
|
+
fileContent,
|
|
64866
|
+
validate,
|
|
64867
|
+
global
|
|
64868
|
+
});
|
|
64869
|
+
}
|
|
64870
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
64871
|
+
return new CommandcodeSubagent({
|
|
64872
|
+
outputRoot,
|
|
64873
|
+
relativeDirPath,
|
|
64874
|
+
relativeFilePath,
|
|
64875
|
+
frontmatter: {
|
|
64876
|
+
name: "",
|
|
64877
|
+
description: ""
|
|
64878
|
+
},
|
|
64879
|
+
body: "",
|
|
64880
|
+
fileContent: "",
|
|
64881
|
+
validate: false
|
|
64882
|
+
});
|
|
64883
|
+
}
|
|
64884
|
+
};
|
|
64885
|
+
//#endregion
|
|
62117
64886
|
//#region src/features/subagents/copilot-subagent.ts
|
|
62118
64887
|
const CopilotSubagentFrontmatterSchema = z.looseObject({
|
|
62119
64888
|
name: z.string(),
|
|
@@ -65014,7 +67783,7 @@ var RooSubagent = class extends ToolSubagent {
|
|
|
65014
67783
|
//#region src/features/subagents/tabnine-subagent.ts
|
|
65015
67784
|
const TabnineSubagentFrontmatterSchema = z.looseObject({
|
|
65016
67785
|
name: z.string(),
|
|
65017
|
-
description: z.
|
|
67786
|
+
description: z.string(),
|
|
65018
67787
|
/** "local" (default) runs inside the CLI; "remote" delegates to a Tabnine cloud agent. */
|
|
65019
67788
|
kind: z.optional(z.string()),
|
|
65020
67789
|
/** Built-in tool names the subagent may use; omitted = every tool. */
|
|
@@ -65073,14 +67842,23 @@ var TabnineSubagent = class TabnineSubagent extends ToolSubagent {
|
|
|
65073
67842
|
validate: true
|
|
65074
67843
|
});
|
|
65075
67844
|
}
|
|
65076
|
-
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
67845
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
65077
67846
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
65078
67847
|
const tabnineSection = rulesyncFrontmatter.tabnine ?? {};
|
|
65079
|
-
const
|
|
67848
|
+
const merged = {
|
|
65080
67849
|
name: rulesyncFrontmatter.name,
|
|
65081
67850
|
description: rulesyncFrontmatter.description,
|
|
65082
67851
|
...tabnineSection
|
|
65083
67852
|
};
|
|
67853
|
+
let description = merged.description;
|
|
67854
|
+
if (description === void 0 || description === "") {
|
|
67855
|
+
description = rulesyncFrontmatter.name ? `${rulesyncFrontmatter.name} subagent` : "subagent";
|
|
67856
|
+
logger?.warn(`Tabnine CLI subagent ${rulesyncSubagent.getRelativeFilePath()} has no description, which Tabnine requires; wrote ${JSON.stringify(description)} as a placeholder.`);
|
|
67857
|
+
}
|
|
67858
|
+
const tabnineSubagentFrontmatter = {
|
|
67859
|
+
...merged,
|
|
67860
|
+
description
|
|
67861
|
+
};
|
|
65084
67862
|
const body = rulesyncSubagent.getBody();
|
|
65085
67863
|
const fileContent = stringifyFrontmatter(body, tabnineSubagentFrontmatter, { avoidBlockScalars: true });
|
|
65086
67864
|
const paths = this.getSettablePaths({ global });
|
|
@@ -65763,6 +68541,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
65763
68541
|
filePattern: "*.toml"
|
|
65764
68542
|
}
|
|
65765
68543
|
}],
|
|
68544
|
+
["commandcode", {
|
|
68545
|
+
class: CommandcodeSubagent,
|
|
68546
|
+
meta: {
|
|
68547
|
+
supportsProject: true,
|
|
68548
|
+
supportsSimulated: false,
|
|
68549
|
+
supportsGlobal: true,
|
|
68550
|
+
filePattern: "*.md"
|
|
68551
|
+
}
|
|
68552
|
+
}],
|
|
65766
68553
|
["copilot", {
|
|
65767
68554
|
class: CopilotSubagent,
|
|
65768
68555
|
meta: {
|
|
@@ -68595,6 +71382,73 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
|
68595
71382
|
}
|
|
68596
71383
|
};
|
|
68597
71384
|
//#endregion
|
|
71385
|
+
//#region src/features/rules/commandcode-rule.ts
|
|
71386
|
+
var CommandcodeRule = class CommandcodeRule extends ToolRule {
|
|
71387
|
+
constructor({ fileContent, root, ...rest }) {
|
|
71388
|
+
super({
|
|
71389
|
+
...rest,
|
|
71390
|
+
fileContent,
|
|
71391
|
+
root: root ?? false
|
|
71392
|
+
});
|
|
71393
|
+
}
|
|
71394
|
+
static getSettablePaths({ global = false } = {}) {
|
|
71395
|
+
return { root: {
|
|
71396
|
+
relativeDirPath: global ? COMMANDCODE_DIR : ".",
|
|
71397
|
+
relativeFilePath: COMMANDCODE_RULE_FILE_NAME
|
|
71398
|
+
} };
|
|
71399
|
+
}
|
|
71400
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
71401
|
+
const { root } = this.getSettablePaths({ global });
|
|
71402
|
+
const relativePath = join(root.relativeDirPath, root.relativeFilePath);
|
|
71403
|
+
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
71404
|
+
return new CommandcodeRule({
|
|
71405
|
+
outputRoot,
|
|
71406
|
+
relativeDirPath: root.relativeDirPath,
|
|
71407
|
+
relativeFilePath: root.relativeFilePath,
|
|
71408
|
+
fileContent,
|
|
71409
|
+
validate,
|
|
71410
|
+
root: true
|
|
71411
|
+
});
|
|
71412
|
+
}
|
|
71413
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
71414
|
+
const { root } = this.getSettablePaths({ global });
|
|
71415
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
71416
|
+
return new CommandcodeRule({
|
|
71417
|
+
outputRoot,
|
|
71418
|
+
relativeDirPath: root.relativeDirPath,
|
|
71419
|
+
relativeFilePath: root.relativeFilePath,
|
|
71420
|
+
fileContent: rulesyncRule.getBody(),
|
|
71421
|
+
validate,
|
|
71422
|
+
root: isRoot
|
|
71423
|
+
});
|
|
71424
|
+
}
|
|
71425
|
+
toRulesyncRule() {
|
|
71426
|
+
return this.toRulesyncRuleDefault();
|
|
71427
|
+
}
|
|
71428
|
+
validate() {
|
|
71429
|
+
return {
|
|
71430
|
+
success: true,
|
|
71431
|
+
error: null
|
|
71432
|
+
};
|
|
71433
|
+
}
|
|
71434
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
71435
|
+
return new CommandcodeRule({
|
|
71436
|
+
outputRoot,
|
|
71437
|
+
relativeDirPath,
|
|
71438
|
+
relativeFilePath,
|
|
71439
|
+
fileContent: "",
|
|
71440
|
+
validate: false,
|
|
71441
|
+
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".commandcode")
|
|
71442
|
+
});
|
|
71443
|
+
}
|
|
71444
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
71445
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
71446
|
+
rulesyncRule,
|
|
71447
|
+
toolTarget: "commandcode"
|
|
71448
|
+
});
|
|
71449
|
+
}
|
|
71450
|
+
};
|
|
71451
|
+
//#endregion
|
|
68598
71452
|
//#region src/features/rules/continue-rule.ts
|
|
68599
71453
|
/**
|
|
68600
71454
|
* Frontmatter schema for Continue rule files (`.continue/rules/*.md`).
|
|
@@ -71343,24 +74197,6 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
71343
74197
|
}
|
|
71344
74198
|
};
|
|
71345
74199
|
//#endregion
|
|
71346
|
-
//#region src/constants/pool-paths.ts
|
|
71347
|
-
/**
|
|
71348
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
71349
|
-
*
|
|
71350
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
71351
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
71352
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
71353
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
71354
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
71355
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
71356
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
71357
|
-
*
|
|
71358
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
71359
|
-
* @see https://github.com/poolsideai/pool
|
|
71360
|
-
*/
|
|
71361
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
71362
|
-
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
71363
|
-
//#endregion
|
|
71364
74200
|
//#region src/features/rules/pool-rule.ts
|
|
71365
74201
|
var PoolRule = class PoolRule extends ToolRule {
|
|
71366
74202
|
static getSettablePaths({ global = false } = {}) {
|
|
@@ -72933,6 +75769,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
72933
75769
|
collisionPolicy: "fold"
|
|
72934
75770
|
}
|
|
72935
75771
|
}],
|
|
75772
|
+
["commandcode", {
|
|
75773
|
+
class: CommandcodeRule,
|
|
75774
|
+
meta: {
|
|
75775
|
+
extension: "md",
|
|
75776
|
+
supportsGlobal: true,
|
|
75777
|
+
ruleDiscoveryMode: "auto",
|
|
75778
|
+
collisionPolicy: "fold"
|
|
75779
|
+
}
|
|
75780
|
+
}],
|
|
72936
75781
|
["continue", {
|
|
72937
75782
|
class: ContinueRule,
|
|
72938
75783
|
meta: {
|
|
@@ -76427,6 +79272,6 @@ async function importChecksCore(params) {
|
|
|
76427
79272
|
return writtenCount;
|
|
76428
79273
|
}
|
|
76429
79274
|
//#endregion
|
|
76430
|
-
export { RulesyncCheck as $, writeFileContent as $t, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SKILLS_RELATIVE_DIR_PATH as An, directoryExists as At, RulesyncSkill as B, truncateText as Bn, listSubdirectoryNames as Bt, ChecksProcessor as C, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Cn, ErrorCodes as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_PERMISSIONS_SCHEMA_URL as Dn, assertWritablePathInsideRoot as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as En, assertTreeContainsNoSymlinks as Et, AUGMENTCODE_DIR as F, parseCommaSeparatedList as Fn, isFileNotFoundError as Ft, RulesyncMcp as G, stripControlCharactersKeepingLineFeeds as Gn, removeDirectoryStrict as Gt, RulesyncRule as H, hasEnclosingMarkOutsideKeycap as Hn, readFileContent as Ht, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as I, ALL_FEATURES as In, isFileSystemError as It, getRulesyncSourceCandidates as J, removeTempDirectory as Jt, RulesyncIgnore as K, stripHiddenCharacters as Kn, removeFile as Kt, getLocalSkillDirNames as L, ALL_FEATURES_WITH_WILDCARD as Ln, isSymlink as Lt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as M, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Mn, fileExists as Mt, caseFoldIdentity as N, RULESYNC_USER_CONFIG_DIR_NAME as Nn, getFileSize as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RELATIVE_DIR_PATH as On, checkPathTraversal as Ot, groupSpellingsByCaseFoldedIdentity as P, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Pn, getHomeDirectory as Pt, RulesyncCommandFrontmatterSchema as Q, writeFileBuffer as Qt, RulesyncSubagent as R, DEPRECATED_FEATURE_REPLACEMENTS as Rn, listDirectoryEntryNames as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_MCP_SCHEMA_URL as Sn, CLIError as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Tn, assertDirectoryIfExists as Tt, RulesyncRuleFrontmatterSchema as U, quoteForLog as Un, readFileContentOrNull as Ut, RulesyncSkillFrontmatterSchema as V, hasDeceptiveHiddenCharacters as Vn, pathEscapesRoot as Vt, RulesyncPermissions as W, stripControlCharacters as Wn, removeDirectory as Wt, parseJsonc as X, runWithDirectoryRollback as Xt, resolveRulesyncSourceWritePath as Y, resolvePath as Yt, RulesyncCommand as Z, toPosixPath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, fallbackLogger as _t, getProcessorRegistryEntry as a, MAX_FILE_SIZE as an, ConfigResolver as at, CommandsProcessor as b, RULESYNC_MCP_LEGACY_FILE_NAME as bn, resetRunWarningState as bt, RulesProcessor as c, RULESYNC_CHECKS_RELATIVE_DIR_PATH as cn, CONFLICTING_TARGET_PAIRS as ct, CODEBUDDY_DIR as d, RULESYNC_CONFIG_SCHEMA_URL as dn, SourceEntrySchema as dt, ALL_TOOL_TARGETS as en, RulesyncCheckFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as fn, assertTargetsFeaturesExclusive as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, WarningCollectingLogger as gt, shortenToWidth as h, RULESYNC_HOOKS_LEGACY_FILE_NAME as hn, JsonLogger as ht, inspectInputRoots as i, CURATED_RULES_FEATURE_SUBDIR as in, SKILL_FILE_NAME as it, FACTORYDROID_DIR as j, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as jn, ensureDir as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_RULES_RELATIVE_DIR_PATH as kn, createTempDirectory as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ln, ConfigFileSchema as lt, displayWidthOf as m, RULESYNC_HOOKS_FILE_NAME as mn, ConsoleLogger as mt, formatSourceLoadFailure as n, PACKAGING_TOOL_TARGETS as nn, loadYaml as nt, convertFromTool as o, RULESYNC_AIIGNORE_FILE_NAME as on, mergeInputRootConfigs as ot, ELLIPSIS_WIDTH as p, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as pn, findControlCharacter as pt, RulesyncHooks as q, removeFileStrict as qt, generate as r, ToolTargetSchema as rn, SHARED_USER_MANAGED_CONFIG_PATHS as rt, isPackagingToolTarget as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as sn, resolveEffectiveInputRoots as st, importFromTool as t, ALL_TOOL_TARGETS_WITH_WILDCARD as tn, stringifyFrontmatter as tt, SkillsProcessor as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as un, GITIGNORE_DESTINATION_KEY as ut,
|
|
79275
|
+
export { RulesyncCheck as $, writeFileContent as $t, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SKILLS_RELATIVE_DIR_PATH as An, directoryExists as At, RulesyncSkill as B, truncateText as Bn, listSubdirectoryNames as Bt, ChecksProcessor as C, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Cn, ErrorCodes as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_PERMISSIONS_SCHEMA_URL as Dn, assertWritablePathInsideRoot as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as En, assertTreeContainsNoSymlinks as Et, AUGMENTCODE_DIR as F, parseCommaSeparatedList as Fn, isFileNotFoundError as Ft, RulesyncMcp as G, stripControlCharactersKeepingLineFeeds as Gn, removeDirectoryStrict as Gt, RulesyncRule as H, hasEnclosingMarkOutsideKeycap as Hn, readFileContent as Ht, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as I, ALL_FEATURES as In, isFileSystemError as It, getRulesyncSourceCandidates as J, removeTempDirectory as Jt, RulesyncIgnore as K, stripHiddenCharacters as Kn, removeFile as Kt, getLocalSkillDirNames as L, ALL_FEATURES_WITH_WILDCARD as Ln, isSymlink as Lt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as M, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Mn, fileExists as Mt, caseFoldIdentity as N, RULESYNC_USER_CONFIG_DIR_NAME as Nn, getFileSize as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RELATIVE_DIR_PATH as On, checkPathTraversal as Ot, groupSpellingsByCaseFoldedIdentity as P, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Pn, getHomeDirectory as Pt, RulesyncCommandFrontmatterSchema as Q, writeFileBuffer as Qt, RulesyncSubagent as R, DEPRECATED_FEATURE_REPLACEMENTS as Rn, listDirectoryEntryNames as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_MCP_SCHEMA_URL as Sn, CLIError as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Tn, assertDirectoryIfExists as Tt, RulesyncRuleFrontmatterSchema as U, quoteForLog as Un, readFileContentOrNull as Ut, RulesyncSkillFrontmatterSchema as V, hasDeceptiveHiddenCharacters as Vn, pathEscapesRoot as Vt, RulesyncPermissions as W, stripControlCharacters as Wn, removeDirectory as Wt, parseJsonc as X, runWithDirectoryRollback as Xt, resolveRulesyncSourceWritePath as Y, resolvePath as Yt, RulesyncCommand as Z, toPosixPath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, fallbackLogger as _t, getProcessorRegistryEntry as a, MAX_FILE_SIZE as an, ConfigResolver as at, CommandsProcessor as b, RULESYNC_MCP_LEGACY_FILE_NAME as bn, resetRunWarningState as bt, RulesProcessor as c, RULESYNC_CHECKS_RELATIVE_DIR_PATH as cn, CONFLICTING_TARGET_PAIRS as ct, CODEBUDDY_DIR as d, RULESYNC_CONFIG_SCHEMA_URL as dn, SourceEntrySchema as dt, ALL_TOOL_TARGETS as en, RulesyncCheckFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as fn, assertTargetsFeaturesExclusive as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, WarningCollectingLogger as gt, shortenToWidth as h, RULESYNC_HOOKS_LEGACY_FILE_NAME as hn, JsonLogger as ht, inspectInputRoots as i, CURATED_RULES_FEATURE_SUBDIR as in, SKILL_FILE_NAME as it, FACTORYDROID_DIR as j, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as jn, ensureDir as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_RULES_RELATIVE_DIR_PATH as kn, createTempDirectory as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ln, ConfigFileSchema as lt, displayWidthOf as m, RULESYNC_HOOKS_FILE_NAME as mn, ConsoleLogger as mt, formatSourceLoadFailure as n, PACKAGING_TOOL_TARGETS as nn, loadYaml as nt, convertFromTool as o, RULESYNC_AIIGNORE_FILE_NAME as on, mergeInputRootConfigs as ot, ELLIPSIS_WIDTH as p, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as pn, findControlCharacter as pt, RulesyncHooks as q, removeFileStrict as qt, generate as r, ToolTargetSchema as rn, SHARED_USER_MANAGED_CONFIG_PATHS as rt, isPackagingToolTarget as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as sn, resolveEffectiveInputRoots as st, importFromTool as t, ALL_TOOL_TARGETS_WITH_WILDCARD as tn, stringifyFrontmatter as tt, SkillsProcessor as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as un, GITIGNORE_DESTINATION_KEY as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, warnOnConflictingFlags as vt, CODEXCLI_BASH_RULES_FILE_NAME as w, RULESYNC_PERMISSIONS_FILE_NAME as wn, applyFileMode as wt, QWENCODE_DIR as x, RULESYNC_MCP_RELATIVE_FILE_PATH as xn, withWarnOnceScope as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_FILE_NAME as yn, withFallbackLoggerTarget as yt, RulesyncSubagentFrontmatterSchema as z, formatError as zn, listFilePathsRecursively as zt };
|
|
76431
79276
|
|
|
76432
|
-
//# sourceMappingURL=import-
|
|
79277
|
+
//# sourceMappingURL=import-De13gNpF.js.map
|