rulesync 16.32.0 → 16.33.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 +2 -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-D5n_V39C.js → import-C6R-lXFF.js} +1856 -567
- package/dist/import-C6R-lXFF.js.map +1 -0
- package/dist/{import-0wSuIchf.cjs → import-DVRU6cLB.cjs} +1871 -582
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-D5n_V39C.js.map +0 -1
|
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
444
444
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
445
445
|
//#endregion
|
|
446
446
|
//#region src/constants/rulesync-paths.ts
|
|
447
|
-
const { join: join$
|
|
447
|
+
const { join: join$342 } = node_path.posix;
|
|
448
448
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
449
449
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
450
450
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
451
451
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
452
452
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
453
453
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
454
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
454
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$342(RULES_FEATURE_SUBDIR, ".curated");
|
|
455
455
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
456
456
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
457
457
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
458
458
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
459
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
460
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
461
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
462
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
463
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
464
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
465
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
466
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
467
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
468
|
-
join$
|
|
469
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
470
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
459
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$342(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
460
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
461
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
462
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
463
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
464
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
465
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
466
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
467
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
468
|
+
join$342(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
469
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
470
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
471
471
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
472
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
472
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
473
473
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
474
474
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
475
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
476
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
475
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
476
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$342(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
477
477
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
478
478
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
479
479
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -583,6 +583,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
583
583
|
"copilot",
|
|
584
584
|
"copilotcli",
|
|
585
585
|
"cortexcode",
|
|
586
|
+
"crush",
|
|
586
587
|
"cursor",
|
|
587
588
|
"deepagents",
|
|
588
589
|
"factorydroid",
|
|
@@ -719,6 +720,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
719
720
|
"musecode",
|
|
720
721
|
"opencode",
|
|
721
722
|
"pi",
|
|
723
|
+
"pool",
|
|
722
724
|
"qwencode",
|
|
723
725
|
"reasonix",
|
|
724
726
|
"replit",
|
|
@@ -749,6 +751,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
749
751
|
"copilot",
|
|
750
752
|
"copilotcli",
|
|
751
753
|
"cortexcode",
|
|
754
|
+
"crush",
|
|
752
755
|
"opencode",
|
|
753
756
|
"pi",
|
|
754
757
|
"factorydroid",
|
|
@@ -781,6 +784,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
781
784
|
"continue",
|
|
782
785
|
"copilot",
|
|
783
786
|
"copilotcli",
|
|
787
|
+
"crush",
|
|
784
788
|
"cursor",
|
|
785
789
|
"deepagents",
|
|
786
790
|
"devin",
|
|
@@ -3308,6 +3312,9 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3308
3312
|
"reasonix.toml",
|
|
3309
3313
|
".vscode/settings.json",
|
|
3310
3314
|
".zed/settings.json",
|
|
3315
|
+
"crush.json",
|
|
3316
|
+
".crush.json",
|
|
3317
|
+
".config/crush/crush.json",
|
|
3311
3318
|
"kilo.json",
|
|
3312
3319
|
"kilo.jsonc",
|
|
3313
3320
|
"opencode.json",
|
|
@@ -3512,6 +3519,27 @@ function omitPrototypePollutionKeys(record) {
|
|
|
3512
3519
|
}
|
|
3513
3520
|
return sanitized;
|
|
3514
3521
|
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
|
|
3524
|
+
* reachable through the value (directly, or through an array) is copied with
|
|
3525
|
+
* its prototype-pollution keys dropped. Scalars and arrays of scalars come back
|
|
3526
|
+
* as they are.
|
|
3527
|
+
*
|
|
3528
|
+
* Use when passing an unknown, user-supplied value through unchanged — an MCP
|
|
3529
|
+
* server field a tool documents that rulesync does not model — so a
|
|
3530
|
+
* `__proto__` key nested anywhere inside it cannot ride into the generated
|
|
3531
|
+
* config, where the consuming tool may merge the object without the same care.
|
|
3532
|
+
*/
|
|
3533
|
+
function omitPrototypePollutionKeysDeep(value) {
|
|
3534
|
+
if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
|
|
3535
|
+
if (value === null || typeof value !== "object") return value;
|
|
3536
|
+
const sanitized = {};
|
|
3537
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3538
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3539
|
+
sanitized[key] = omitPrototypePollutionKeysDeep(entry);
|
|
3540
|
+
}
|
|
3541
|
+
return sanitized;
|
|
3542
|
+
}
|
|
3515
3543
|
//#endregion
|
|
3516
3544
|
//#region src/utils/type-guards.ts
|
|
3517
3545
|
/**
|
|
@@ -4579,6 +4607,14 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4579
4607
|
"stop"
|
|
4580
4608
|
];
|
|
4581
4609
|
/**
|
|
4610
|
+
* Hook events supported by Crush.
|
|
4611
|
+
*
|
|
4612
|
+
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
4613
|
+
* (Claude Code-compatible payload; `matcher` is a regex on the tool name).
|
|
4614
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
4615
|
+
*/
|
|
4616
|
+
const CRUSH_HOOK_EVENTS = ["preToolUse"];
|
|
4617
|
+
/**
|
|
4582
4618
|
* Hook events supported by JetBrains Junie CLI.
|
|
4583
4619
|
*
|
|
4584
4620
|
* Junie CLI exposes seven lifecycle events under the `"hooks"` key of
|
|
@@ -5121,6 +5157,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
5121
5157
|
grokcli: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5122
5158
|
"kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5123
5159
|
zcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5160
|
+
crush: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5124
5161
|
bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5125
5162
|
cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5126
5163
|
continue: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
@@ -5618,6 +5655,13 @@ const VIBE_TO_CANONICAL_EVENT_NAMES = {
|
|
|
5618
5655
|
post_agent_turn: "stop"
|
|
5619
5656
|
};
|
|
5620
5657
|
/**
|
|
5658
|
+
* Canonical -> Crush event names. Crush spells its events the Claude Code
|
|
5659
|
+
* way (`PreToolUse`).
|
|
5660
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
5661
|
+
*/
|
|
5662
|
+
const CANONICAL_TO_CRUSH_EVENT_NAMES = { preToolUse: "PreToolUse" };
|
|
5663
|
+
const CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5664
|
+
/**
|
|
5621
5665
|
* Map canonical camelCase event names to Qwen Code PascalCase.
|
|
5622
5666
|
*
|
|
5623
5667
|
* Qwen Code reuses the same Claude-style PascalCase event names for the events
|
|
@@ -6057,6 +6101,11 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
6057
6101
|
kiroAutoBlock: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6058
6102
|
musecodeMode: zod_mini.z.optional(zod_mini.z.enum(["required", "optional"])),
|
|
6059
6103
|
rovodevEnableInstructions: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6104
|
+
crushOauth: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6105
|
+
crushOauthClientId: zod_mini.z.optional(zod_mini.z.string()),
|
|
6106
|
+
crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
|
|
6107
|
+
crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
|
|
6108
|
+
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6060
6109
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
6061
6110
|
/**
|
|
6062
6111
|
* The canonical per-server tool allowlist.
|
|
@@ -6133,6 +6182,8 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
6133
6182
|
continue: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6134
6183
|
copilot: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6135
6184
|
copilotcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6185
|
+
cortexcode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6186
|
+
crush: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6136
6187
|
cursor: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6137
6188
|
deepagents: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6138
6189
|
devin: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -6149,7 +6200,6 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
6149
6200
|
reasonix: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6150
6201
|
roo: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6151
6202
|
rovodev: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6152
|
-
cortexcode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6153
6203
|
tabnine: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6154
6204
|
takt: zod_mini.z.optional(toolScopedMcpSchema),
|
|
6155
6205
|
vibe: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -6481,6 +6531,15 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6481
6531
|
"musecodeMode",
|
|
6482
6532
|
"rovodevEnableInstructions",
|
|
6483
6533
|
"enable_instructions",
|
|
6534
|
+
"crushOauth",
|
|
6535
|
+
"crushOauthClientId",
|
|
6536
|
+
"crushOauthClientSecret",
|
|
6537
|
+
"crushOauthCallbackPort",
|
|
6538
|
+
"crushSessionless",
|
|
6539
|
+
"oauth_client_id",
|
|
6540
|
+
"oauth_client_secret",
|
|
6541
|
+
"oauth_callback_port",
|
|
6542
|
+
"sessionless",
|
|
6484
6543
|
"enabled"
|
|
6485
6544
|
])];
|
|
6486
6545
|
}));
|
|
@@ -6929,7 +6988,7 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6929
6988
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
6930
6989
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
6931
6990
|
* `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
|
|
6932
|
-
* `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
6991
|
+
* `todoWrite`, `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
6933
6992
|
* `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
|
|
6934
6993
|
* `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
|
|
6935
6994
|
* `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
|
|
@@ -6942,7 +7001,7 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6942
7001
|
* changes what the file said is reported in either scope, naming what that key
|
|
6943
7002
|
* decides there — the autonomy and containment
|
|
6944
7003
|
* controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
|
|
6945
|
-
* registry controls (`disabled`, `visible`, `listDirectory`, and `eager`, which
|
|
7004
|
+
* registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
|
|
6946
7005
|
* demotes an omitted tool to deferred rather than removing it, replacing rather
|
|
6947
7006
|
* than merging with the list a higher scope set), the Auto Mode
|
|
6948
7007
|
* classifier config, and, because these groups are loose objects, any key
|
|
@@ -6983,6 +7042,14 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6983
7042
|
* `looseObject` (verbatim passthrough). Both project and global scope are
|
|
6984
7043
|
* supported.
|
|
6985
7044
|
*
|
|
7045
|
+
* Only `tools` and `general` are written; any other key is ignored with a
|
|
7046
|
+
* warning. Within them, the paths Tabnine CLI runs as a command or sends its
|
|
7047
|
+
* traffic to — `tools.discoveryCommand`, `tools.callCommand`,
|
|
7048
|
+
* `tools.shell.pager`, `tools.sandbox.command`, `general.tabnineHost` and
|
|
7049
|
+
* `general.preferredEditor` — are refused with a warning rather than written,
|
|
7050
|
+
* so a fetched permissions file cannot point the CLI at an executable or a
|
|
7051
|
+
* server of its choosing; set those by hand in `settings.json`.
|
|
7052
|
+
*
|
|
6986
7053
|
* @example
|
|
6987
7054
|
* { "tools": { "core": ["read_file", "run_shell_command(git)"] },
|
|
6988
7055
|
* "general": { "defaultApprovalMode": "plan" } }
|
|
@@ -7767,6 +7834,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
7767
7834
|
continue: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7768
7835
|
copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7769
7836
|
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7837
|
+
crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7770
7838
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7771
7839
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7772
7840
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
@@ -12792,6 +12860,26 @@ const ZCODE_USER_CONFIG_DECLARATION = {
|
|
|
12792
12860
|
}
|
|
12793
12861
|
};
|
|
12794
12862
|
/**
|
|
12863
|
+
* Crush's JSON config, shared by the project (`crush.json` / `.crush.json`)
|
|
12864
|
+
* and global (`~/.config/crush/crush.json`) spellings so a policy edit lands
|
|
12865
|
+
* on both. See the `SHARED_CONFIG_OWNERSHIP` entries for the key rationale.
|
|
12866
|
+
*/
|
|
12867
|
+
const CRUSH_CONFIG_DECLARATION = {
|
|
12868
|
+
format: "json",
|
|
12869
|
+
invalidRootPolicy: "error",
|
|
12870
|
+
features: {
|
|
12871
|
+
mcp: {
|
|
12872
|
+
kind: "replace-owned-keys",
|
|
12873
|
+
ownedKeys: ["mcp"]
|
|
12874
|
+
},
|
|
12875
|
+
hooks: {
|
|
12876
|
+
kind: "replace-owned-keys",
|
|
12877
|
+
ownedKeys: ["hooks"]
|
|
12878
|
+
},
|
|
12879
|
+
permissions: { kind: "deep-merge" }
|
|
12880
|
+
}
|
|
12881
|
+
};
|
|
12882
|
+
/**
|
|
12795
12883
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
12796
12884
|
* and both validate against the one published schema, which the gateway
|
|
12797
12885
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13052,6 +13140,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13052
13140
|
},
|
|
13053
13141
|
".zcode/config.json": ZCODE_WORKSPACE_CONFIG_DECLARATION,
|
|
13054
13142
|
".zcode/cli/config.json": ZCODE_USER_CONFIG_DECLARATION,
|
|
13143
|
+
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13144
|
+
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13055
13145
|
".bob/settings.json": {
|
|
13056
13146
|
format: "json",
|
|
13057
13147
|
invalidRootPolicy: "error",
|
|
@@ -18901,15 +18991,12 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
|
|
|
18901
18991
|
return this.body;
|
|
18902
18992
|
}
|
|
18903
18993
|
getFrontmatter() {
|
|
18904
|
-
return
|
|
18905
|
-
description: this.frontmatter.description,
|
|
18906
|
-
prompt: this.frontmatter.prompt
|
|
18907
|
-
};
|
|
18994
|
+
return this.frontmatter;
|
|
18908
18995
|
}
|
|
18909
18996
|
toRulesyncCommand() {
|
|
18910
18997
|
const { description, prompt: _prompt, ...restFields } = this.frontmatter;
|
|
18911
18998
|
const rulesyncFrontmatter = {
|
|
18912
|
-
targets: ["
|
|
18999
|
+
targets: ["*"],
|
|
18913
19000
|
description,
|
|
18914
19001
|
...Object.keys(restFields).length > 0 && { tabnine: restFields }
|
|
18915
19002
|
};
|
|
@@ -20872,7 +20959,20 @@ function stableJson(value) {
|
|
|
20872
20959
|
*/
|
|
20873
20960
|
function isSafeStringRecord(value) {
|
|
20874
20961
|
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
20875
|
-
return Object.entries(value).every(([key, entry]) =>
|
|
20962
|
+
return Object.entries(value).every(([key, entry]) => isSafeEnvEntry({
|
|
20963
|
+
key,
|
|
20964
|
+
value: entry
|
|
20965
|
+
}));
|
|
20966
|
+
}
|
|
20967
|
+
/**
|
|
20968
|
+
* Whether one `env` entry survives the rule {@link isSafeStringRecord} applies
|
|
20969
|
+
* to the whole map. Exported for the adapters that hand a tool an environment
|
|
20970
|
+
* block outside the shared converter (Tabnine, whose hook shape it does not
|
|
20971
|
+
* express) so they drop the same entries this one does.
|
|
20972
|
+
*/
|
|
20973
|
+
function isSafeEnvEntry(entry) {
|
|
20974
|
+
const { key, value } = entry;
|
|
20975
|
+
return typeof value === "string" && key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || value.includes(char));
|
|
20876
20976
|
}
|
|
20877
20977
|
/**
|
|
20878
20978
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
@@ -21763,135 +21863,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
21763
21863
|
}
|
|
21764
21864
|
};
|
|
21765
21865
|
//#endregion
|
|
21766
|
-
//#region src/features/hooks/bob-hooks.ts
|
|
21767
|
-
const BOB_CONVERTER_CONFIG = {
|
|
21768
|
-
supportedEvents: BOB_HOOK_EVENTS,
|
|
21769
|
-
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
21770
|
-
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
21771
|
-
projectDirVar: "",
|
|
21772
|
-
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
21773
|
-
"sessionStart",
|
|
21774
|
-
"beforeSubmitPrompt",
|
|
21775
|
-
"stop"
|
|
21776
|
-
]),
|
|
21777
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
21778
|
-
wildcardMatcherMeansAll: true
|
|
21779
|
-
};
|
|
21780
|
-
/**
|
|
21781
|
-
* Single spelling of the settings.json codec/policy: fail closed on an
|
|
21782
|
-
* unparseable root rather than replacing the user's Bob settings with
|
|
21783
|
-
* generated output.
|
|
21784
|
-
*/
|
|
21785
|
-
function parseBobSettings(fileContent, filePath) {
|
|
21786
|
-
return parseSharedConfig({
|
|
21787
|
-
format: "json",
|
|
21788
|
-
fileContent,
|
|
21789
|
-
filePath,
|
|
21790
|
-
invalidRootPolicy: "error"
|
|
21791
|
-
});
|
|
21792
|
-
}
|
|
21793
|
-
/**
|
|
21794
|
-
* IBM Bob lifecycle hooks.
|
|
21795
|
-
*
|
|
21796
|
-
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
21797
|
-
* `<project>/.bob/settings.json` (project scope) and
|
|
21798
|
-
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
21799
|
-
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
21800
|
-
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
21801
|
-
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
21802
|
-
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
21803
|
-
*
|
|
21804
|
-
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
21805
|
-
*/
|
|
21806
|
-
var BobHooks = class BobHooks extends ToolHooks {
|
|
21807
|
-
constructor(params) {
|
|
21808
|
-
super({
|
|
21809
|
-
...params,
|
|
21810
|
-
fileContent: params.fileContent ?? "{}"
|
|
21811
|
-
});
|
|
21812
|
-
}
|
|
21813
|
-
isDeletable() {
|
|
21814
|
-
return false;
|
|
21815
|
-
}
|
|
21816
|
-
static getSettablePaths({ global = false } = {}) {
|
|
21817
|
-
return {
|
|
21818
|
-
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
21819
|
-
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
21820
|
-
};
|
|
21821
|
-
}
|
|
21822
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
21823
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21824
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
21825
|
-
return new BobHooks({
|
|
21826
|
-
outputRoot,
|
|
21827
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21828
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21829
|
-
fileContent,
|
|
21830
|
-
validate
|
|
21831
|
-
});
|
|
21832
|
-
}
|
|
21833
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
21834
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21835
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21836
|
-
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
21837
|
-
const config = rulesyncHooks.getJson();
|
|
21838
|
-
const hooks = canonicalToToolHooks({
|
|
21839
|
-
config,
|
|
21840
|
-
toolOverrideHooks: config.bob?.hooks,
|
|
21841
|
-
converterConfig: BOB_CONVERTER_CONFIG,
|
|
21842
|
-
logger
|
|
21843
|
-
});
|
|
21844
|
-
const fileContent = applySharedConfigPatch({
|
|
21845
|
-
fileKey: sharedConfigFileKey(paths),
|
|
21846
|
-
feature: "hooks",
|
|
21847
|
-
existingContent,
|
|
21848
|
-
patch: { hooks },
|
|
21849
|
-
filePath,
|
|
21850
|
-
logger
|
|
21851
|
-
});
|
|
21852
|
-
return new BobHooks({
|
|
21853
|
-
outputRoot,
|
|
21854
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21855
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21856
|
-
fileContent,
|
|
21857
|
-
validate
|
|
21858
|
-
});
|
|
21859
|
-
}
|
|
21860
|
-
toRulesyncHooks({ logger } = {}) {
|
|
21861
|
-
const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
21862
|
-
let settings;
|
|
21863
|
-
try {
|
|
21864
|
-
settings = parseBobSettings(this.getFileContent(), configPath);
|
|
21865
|
-
} catch (error) {
|
|
21866
|
-
throw new Error(`Failed to parse Bob hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
21867
|
-
}
|
|
21868
|
-
const hooks = toolHooksToCanonical({
|
|
21869
|
-
logger,
|
|
21870
|
-
hooks: settings.hooks,
|
|
21871
|
-
converterConfig: BOB_CONVERTER_CONFIG
|
|
21872
|
-
});
|
|
21873
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
21874
|
-
hooks,
|
|
21875
|
-
overrideKey: "bob"
|
|
21876
|
-
}), null, 2) });
|
|
21877
|
-
}
|
|
21878
|
-
validate() {
|
|
21879
|
-
return {
|
|
21880
|
-
success: true,
|
|
21881
|
-
error: null
|
|
21882
|
-
};
|
|
21883
|
-
}
|
|
21884
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
21885
|
-
return new BobHooks({
|
|
21886
|
-
outputRoot,
|
|
21887
|
-
relativeDirPath,
|
|
21888
|
-
relativeFilePath,
|
|
21889
|
-
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
21890
|
-
validate: false
|
|
21891
|
-
});
|
|
21892
|
-
}
|
|
21893
|
-
};
|
|
21894
|
-
//#endregion
|
|
21895
21866
|
//#region src/features/hooks/preserve-unowned-hook-commands.ts
|
|
21896
21867
|
/**
|
|
21897
21868
|
* Read the `hooks` value from a destination JSON file.
|
|
@@ -22178,75 +22149,22 @@ function warnSkip({ logger, event, expected }) {
|
|
|
22178
22149
|
logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
|
|
22179
22150
|
}
|
|
22180
22151
|
//#endregion
|
|
22181
|
-
//#region src/features/hooks/
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
|
|
22185
|
-
|
|
22186
|
-
|
|
22187
|
-
|
|
22188
|
-
|
|
22189
|
-
|
|
22190
|
-
|
|
22191
|
-
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
"beforeSubmitPrompt",
|
|
22198
|
-
"stop"
|
|
22199
|
-
]),
|
|
22200
|
-
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22201
|
-
"command",
|
|
22202
|
-
"prompt",
|
|
22203
|
-
"http",
|
|
22204
|
-
"mcp_tool",
|
|
22205
|
-
"agent"
|
|
22206
|
-
]),
|
|
22207
|
-
emitsPromptModel: true,
|
|
22208
|
-
stringPassthroughFields: [
|
|
22209
|
-
{
|
|
22210
|
-
canonical: "if",
|
|
22211
|
-
tool: "if"
|
|
22212
|
-
},
|
|
22213
|
-
{
|
|
22214
|
-
canonical: "statusMessage",
|
|
22215
|
-
tool: "statusMessage"
|
|
22216
|
-
},
|
|
22217
|
-
{
|
|
22218
|
-
canonical: "shell",
|
|
22219
|
-
tool: "shell",
|
|
22220
|
-
commandOnly: true
|
|
22221
|
-
}
|
|
22222
|
-
],
|
|
22223
|
-
booleanPassthroughFields: [
|
|
22224
|
-
{
|
|
22225
|
-
canonical: "once",
|
|
22226
|
-
tool: "once"
|
|
22227
|
-
},
|
|
22228
|
-
{
|
|
22229
|
-
canonical: "async",
|
|
22230
|
-
tool: "async",
|
|
22231
|
-
commandOnly: true
|
|
22232
|
-
},
|
|
22233
|
-
{
|
|
22234
|
-
canonical: "asyncRewake",
|
|
22235
|
-
tool: "asyncRewake",
|
|
22236
|
-
commandOnly: true
|
|
22237
|
-
},
|
|
22238
|
-
{
|
|
22239
|
-
canonical: "continueOnBlock",
|
|
22240
|
-
tool: "continueOnBlock"
|
|
22241
|
-
}
|
|
22242
|
-
],
|
|
22243
|
-
arrayPassthroughFields: [{
|
|
22244
|
-
canonical: "args",
|
|
22245
|
-
tool: "args",
|
|
22246
|
-
commandOnly: true
|
|
22247
|
-
}]
|
|
22248
|
-
};
|
|
22249
|
-
var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
22152
|
+
//#region src/features/hooks/settings-json-hooks.ts
|
|
22153
|
+
/**
|
|
22154
|
+
* Shared implementation for the tools whose hooks live under the top-level
|
|
22155
|
+
* `hooks` key of a JSON settings file in Claude Code's shape —
|
|
22156
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command", ... }] }] }`
|
|
22157
|
+
* — and whose file also holds settings rulesync does not own, so generation
|
|
22158
|
+
* merges the `hooks` key into it (see `SHARED_CONFIG_OWNERSHIP`) instead of
|
|
22159
|
+
* overwriting it, and `--delete` never removes the file wholesale.
|
|
22160
|
+
*
|
|
22161
|
+
* A concrete adapter supplies its {@link SettingsJsonHooksSpec} and its
|
|
22162
|
+
* settable paths; the event mapping, the matcher rules and the per-hook
|
|
22163
|
+
* fields the tool documents are all expressed in the spec's converter config.
|
|
22164
|
+
* An adapter that also supports preserving unowned hooks (Claude Code) opts in
|
|
22165
|
+
* through {@link ToolHooks.supportsPreserveUnowned}.
|
|
22166
|
+
*/
|
|
22167
|
+
var SettingsJsonHooks = class extends ToolHooks {
|
|
22250
22168
|
constructor(params) {
|
|
22251
22169
|
super({
|
|
22252
22170
|
...params,
|
|
@@ -22256,24 +22174,42 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22256
22174
|
isDeletable() {
|
|
22257
22175
|
return false;
|
|
22258
22176
|
}
|
|
22177
|
+
/** The tool-specific half of the adapter. Every concrete class overrides this. */
|
|
22178
|
+
static getSpec() {
|
|
22179
|
+
throw new Error(`${this.name} does not define getSpec()`);
|
|
22180
|
+
}
|
|
22259
22181
|
/**
|
|
22260
|
-
* The converter config used for both directions.
|
|
22261
|
-
*
|
|
22262
|
-
*
|
|
22182
|
+
* The converter config used for both directions. A separate hook so a
|
|
22183
|
+
* subclass can swap tool-specific details (e.g. the project directory
|
|
22184
|
+
* variable) without redefining the whole spec.
|
|
22263
22185
|
*/
|
|
22264
22186
|
static getConverterConfig() {
|
|
22265
|
-
return
|
|
22187
|
+
return this.getSpec().converterConfig;
|
|
22266
22188
|
}
|
|
22267
22189
|
static getSettablePaths(_options = {}) {
|
|
22268
|
-
|
|
22269
|
-
|
|
22270
|
-
|
|
22271
|
-
|
|
22190
|
+
throw new Error(`${this.name} does not define getSettablePaths()`);
|
|
22191
|
+
}
|
|
22192
|
+
/**
|
|
22193
|
+
* The `SHARED_CONFIG_OWNERSHIP` key the settings file is merged under. By
|
|
22194
|
+
* default it is derived from the settable paths; an adapter whose file is
|
|
22195
|
+
* declared under another tool's key (the Claude Code plugin bundle) overrides
|
|
22196
|
+
* it.
|
|
22197
|
+
*/
|
|
22198
|
+
static getSharedFileKey(paths) {
|
|
22199
|
+
return sharedConfigFileKey(paths);
|
|
22200
|
+
}
|
|
22201
|
+
/**
|
|
22202
|
+
* `new this(params)` for the concrete adapter the static method was called
|
|
22203
|
+
* on; the cast is what an abstract class needs to be constructed through
|
|
22204
|
+
* `this`, and lives in one place.
|
|
22205
|
+
*/
|
|
22206
|
+
static instantiate(params) {
|
|
22207
|
+
return new this(params);
|
|
22272
22208
|
}
|
|
22273
22209
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22274
22210
|
const paths = this.getSettablePaths({ global });
|
|
22275
22211
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22276
|
-
return
|
|
22212
|
+
return this.instantiate({
|
|
22277
22213
|
outputRoot,
|
|
22278
22214
|
relativeDirPath: paths.relativeDirPath,
|
|
22279
22215
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22281,40 +22217,41 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22281
22217
|
validate
|
|
22282
22218
|
});
|
|
22283
22219
|
}
|
|
22284
|
-
static supportsPreserveUnowned() {
|
|
22285
|
-
return true;
|
|
22286
|
-
}
|
|
22287
22220
|
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
22288
|
-
return toolHooks instanceof
|
|
22221
|
+
return toolHooks instanceof this ? toolHooks.getOwnershipLockFiles() : [];
|
|
22289
22222
|
}
|
|
22290
22223
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
22291
22224
|
const paths = this.getSettablePaths({ global });
|
|
22292
22225
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22293
22226
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22294
22227
|
const config = rulesyncHooks.getJson();
|
|
22295
|
-
const
|
|
22228
|
+
const generatedHooks = canonicalToToolHooks({
|
|
22296
22229
|
config,
|
|
22297
|
-
toolOverrideHooks:
|
|
22230
|
+
toolOverrideHooks: overrideHooksOf({
|
|
22231
|
+
config,
|
|
22232
|
+
overrideKey: this.getSpec().overrideKey
|
|
22233
|
+
}),
|
|
22298
22234
|
converterConfig: this.getConverterConfig(),
|
|
22299
22235
|
logger
|
|
22300
22236
|
});
|
|
22301
22237
|
const preserving = preserveUnowned && this.supportsPreserveUnowned();
|
|
22302
22238
|
const merged = mergeGeneratedHookLists({
|
|
22303
22239
|
existingContent,
|
|
22304
|
-
generatedHooks
|
|
22240
|
+
generatedHooks,
|
|
22305
22241
|
shape: "matcher-groups",
|
|
22306
22242
|
preserveUnowned: preserving,
|
|
22307
22243
|
previouslyOwned: preserving ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
22308
22244
|
logger
|
|
22309
22245
|
});
|
|
22310
22246
|
const fileContent = applySharedConfigPatch({
|
|
22311
|
-
fileKey:
|
|
22247
|
+
fileKey: this.getSharedFileKey(paths),
|
|
22312
22248
|
feature: "hooks",
|
|
22313
22249
|
existingContent,
|
|
22314
22250
|
patch: { hooks: merged.hooks },
|
|
22315
|
-
filePath
|
|
22251
|
+
filePath,
|
|
22252
|
+
logger
|
|
22316
22253
|
});
|
|
22317
|
-
return
|
|
22254
|
+
return this.instantiate({
|
|
22318
22255
|
outputRoot,
|
|
22319
22256
|
relativeDirPath: paths.relativeDirPath,
|
|
22320
22257
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22324,20 +22261,29 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22324
22261
|
});
|
|
22325
22262
|
}
|
|
22326
22263
|
toRulesyncHooks({ logger } = {}) {
|
|
22264
|
+
const ctor = this.constructor;
|
|
22265
|
+
const spec = ctor.getSpec();
|
|
22266
|
+
const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
22327
22267
|
let settings;
|
|
22328
22268
|
try {
|
|
22329
|
-
settings =
|
|
22269
|
+
settings = parseSharedConfig({
|
|
22270
|
+
format: "json",
|
|
22271
|
+
fileContent: this.getFileContent(),
|
|
22272
|
+
filePath: configPath,
|
|
22273
|
+
invalidRootPolicy: "error"
|
|
22274
|
+
});
|
|
22330
22275
|
} catch (error) {
|
|
22331
|
-
|
|
22276
|
+
const reason = error instanceof Error && error.cause instanceof Error ? error.cause : error;
|
|
22277
|
+
throw new Error(`Failed to parse ${spec.displayName} hooks content in ${configPath}: ${formatError(reason)}`, { cause: error });
|
|
22332
22278
|
}
|
|
22333
22279
|
const hooks = toolHooksToCanonical({
|
|
22280
|
+
logger,
|
|
22334
22281
|
hooks: settings.hooks,
|
|
22335
|
-
converterConfig:
|
|
22336
|
-
logger
|
|
22282
|
+
converterConfig: ctor.getConverterConfig()
|
|
22337
22283
|
});
|
|
22338
22284
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
22339
22285
|
hooks,
|
|
22340
|
-
overrideKey:
|
|
22286
|
+
overrideKey: spec.overrideKey
|
|
22341
22287
|
}), null, 2) });
|
|
22342
22288
|
}
|
|
22343
22289
|
validate() {
|
|
@@ -22347,7 +22293,7 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22347
22293
|
};
|
|
22348
22294
|
}
|
|
22349
22295
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
22350
|
-
return
|
|
22296
|
+
return this.instantiate({
|
|
22351
22297
|
outputRoot,
|
|
22352
22298
|
relativeDirPath,
|
|
22353
22299
|
relativeFilePath,
|
|
@@ -22356,6 +22302,158 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22356
22302
|
});
|
|
22357
22303
|
}
|
|
22358
22304
|
};
|
|
22305
|
+
/**
|
|
22306
|
+
* The `hooks` map of a tool's override block (`config.<overrideKey>.hooks`),
|
|
22307
|
+
* or `undefined` when the block states none. The schema keeps every override
|
|
22308
|
+
* block loose, so the lookup goes through the record form.
|
|
22309
|
+
*/
|
|
22310
|
+
function overrideHooksOf({ config, overrideKey }) {
|
|
22311
|
+
const override = lookupOwn({
|
|
22312
|
+
record: config,
|
|
22313
|
+
key: overrideKey
|
|
22314
|
+
});
|
|
22315
|
+
if (!isPlainObject$1(override)) return;
|
|
22316
|
+
const hooks = override.hooks;
|
|
22317
|
+
return isPlainObject$1(hooks) ? hooks : void 0;
|
|
22318
|
+
}
|
|
22319
|
+
//#endregion
|
|
22320
|
+
//#region src/features/hooks/bob-hooks.ts
|
|
22321
|
+
const BOB_SPEC = {
|
|
22322
|
+
displayName: "Bob",
|
|
22323
|
+
overrideKey: "bob",
|
|
22324
|
+
converterConfig: {
|
|
22325
|
+
supportedEvents: BOB_HOOK_EVENTS,
|
|
22326
|
+
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
22327
|
+
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
22328
|
+
projectDirVar: "",
|
|
22329
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22330
|
+
"sessionStart",
|
|
22331
|
+
"beforeSubmitPrompt",
|
|
22332
|
+
"stop"
|
|
22333
|
+
]),
|
|
22334
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
22335
|
+
wildcardMatcherMeansAll: true
|
|
22336
|
+
}
|
|
22337
|
+
};
|
|
22338
|
+
/**
|
|
22339
|
+
* IBM Bob lifecycle hooks.
|
|
22340
|
+
*
|
|
22341
|
+
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
22342
|
+
* `<project>/.bob/settings.json` (project scope) and
|
|
22343
|
+
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
22344
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
22345
|
+
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
22346
|
+
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
22347
|
+
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
22348
|
+
*
|
|
22349
|
+
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
22350
|
+
*/
|
|
22351
|
+
var BobHooks = class extends SettingsJsonHooks {
|
|
22352
|
+
static getSpec() {
|
|
22353
|
+
return BOB_SPEC;
|
|
22354
|
+
}
|
|
22355
|
+
static getSettablePaths({ global = false } = {}) {
|
|
22356
|
+
return {
|
|
22357
|
+
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
22358
|
+
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
22359
|
+
};
|
|
22360
|
+
}
|
|
22361
|
+
};
|
|
22362
|
+
//#endregion
|
|
22363
|
+
//#region src/features/hooks/claudecode-hooks.ts
|
|
22364
|
+
const CLAUDE_SPEC = {
|
|
22365
|
+
displayName: "Claude",
|
|
22366
|
+
overrideKey: "claudecode",
|
|
22367
|
+
converterConfig: {
|
|
22368
|
+
supportedEvents: CLAUDE_HOOK_EVENTS,
|
|
22369
|
+
canonicalToToolEventNames: CANONICAL_TO_CLAUDE_EVENT_NAMES,
|
|
22370
|
+
toolToCanonicalEventNames: CLAUDE_TO_CANONICAL_EVENT_NAMES,
|
|
22371
|
+
projectDirVar: "$CLAUDE_PROJECT_DIR",
|
|
22372
|
+
prefixDotRelativeCommandsOnly: true,
|
|
22373
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22374
|
+
"worktreeCreate",
|
|
22375
|
+
"worktreeRemove",
|
|
22376
|
+
"messageDisplay",
|
|
22377
|
+
"postToolBatch",
|
|
22378
|
+
"taskCreated",
|
|
22379
|
+
"taskCompleted",
|
|
22380
|
+
"teammateIdle",
|
|
22381
|
+
"cwdChanged",
|
|
22382
|
+
"beforeSubmitPrompt",
|
|
22383
|
+
"stop"
|
|
22384
|
+
]),
|
|
22385
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22386
|
+
"command",
|
|
22387
|
+
"prompt",
|
|
22388
|
+
"http",
|
|
22389
|
+
"mcp_tool",
|
|
22390
|
+
"agent"
|
|
22391
|
+
]),
|
|
22392
|
+
emitsPromptModel: true,
|
|
22393
|
+
stringPassthroughFields: [
|
|
22394
|
+
{
|
|
22395
|
+
canonical: "if",
|
|
22396
|
+
tool: "if"
|
|
22397
|
+
},
|
|
22398
|
+
{
|
|
22399
|
+
canonical: "statusMessage",
|
|
22400
|
+
tool: "statusMessage"
|
|
22401
|
+
},
|
|
22402
|
+
{
|
|
22403
|
+
canonical: "shell",
|
|
22404
|
+
tool: "shell",
|
|
22405
|
+
commandOnly: true
|
|
22406
|
+
}
|
|
22407
|
+
],
|
|
22408
|
+
booleanPassthroughFields: [
|
|
22409
|
+
{
|
|
22410
|
+
canonical: "once",
|
|
22411
|
+
tool: "once"
|
|
22412
|
+
},
|
|
22413
|
+
{
|
|
22414
|
+
canonical: "async",
|
|
22415
|
+
tool: "async",
|
|
22416
|
+
commandOnly: true
|
|
22417
|
+
},
|
|
22418
|
+
{
|
|
22419
|
+
canonical: "asyncRewake",
|
|
22420
|
+
tool: "asyncRewake",
|
|
22421
|
+
commandOnly: true
|
|
22422
|
+
},
|
|
22423
|
+
{
|
|
22424
|
+
canonical: "continueOnBlock",
|
|
22425
|
+
tool: "continueOnBlock"
|
|
22426
|
+
}
|
|
22427
|
+
],
|
|
22428
|
+
arrayPassthroughFields: [{
|
|
22429
|
+
canonical: "args",
|
|
22430
|
+
tool: "args",
|
|
22431
|
+
commandOnly: true
|
|
22432
|
+
}]
|
|
22433
|
+
}
|
|
22434
|
+
};
|
|
22435
|
+
var ClaudecodeHooks = class extends SettingsJsonHooks {
|
|
22436
|
+
static getSpec() {
|
|
22437
|
+
return CLAUDE_SPEC;
|
|
22438
|
+
}
|
|
22439
|
+
static getSettablePaths(_options = {}) {
|
|
22440
|
+
return {
|
|
22441
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
22442
|
+
relativeFilePath: CLAUDECODE_SETTINGS_FILE_NAME
|
|
22443
|
+
};
|
|
22444
|
+
}
|
|
22445
|
+
/**
|
|
22446
|
+
* Every Claude-shaped settings file rulesync writes for Claude Code — the
|
|
22447
|
+
* project and user `settings.json` and the plugin bundle's `hooks.json` — is
|
|
22448
|
+
* declared under the one Claude key in `SHARED_CONFIG_OWNERSHIP`.
|
|
22449
|
+
*/
|
|
22450
|
+
static getSharedFileKey(_paths) {
|
|
22451
|
+
return CLAUDE_SETTINGS_SHARED_FILE_KEY;
|
|
22452
|
+
}
|
|
22453
|
+
static supportsPreserveUnowned() {
|
|
22454
|
+
return true;
|
|
22455
|
+
}
|
|
22456
|
+
};
|
|
22359
22457
|
//#endregion
|
|
22360
22458
|
//#region src/features/hooks/claudecode-plugin-hooks.ts
|
|
22361
22459
|
var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
@@ -22883,54 +22981,45 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
22883
22981
|
};
|
|
22884
22982
|
//#endregion
|
|
22885
22983
|
//#region src/features/hooks/continue-hooks.ts
|
|
22886
|
-
const
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
"
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
22908
|
-
|
|
22909
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
22918
|
-
|
|
22984
|
+
const CONTINUE_SPEC = {
|
|
22985
|
+
displayName: "Continue",
|
|
22986
|
+
overrideKey: "continue",
|
|
22987
|
+
converterConfig: {
|
|
22988
|
+
supportedEvents: CONTINUE_HOOK_EVENTS,
|
|
22989
|
+
canonicalToToolEventNames: CANONICAL_TO_CONTINUE_EVENT_NAMES,
|
|
22990
|
+
toolToCanonicalEventNames: CONTINUE_TO_CANONICAL_EVENT_NAMES,
|
|
22991
|
+
projectDirVar: "$CONTINUE_PROJECT_DIR",
|
|
22992
|
+
prefixDotRelativeCommandsOnly: true,
|
|
22993
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22994
|
+
"beforeSubmitPrompt",
|
|
22995
|
+
"stop",
|
|
22996
|
+
"teammateIdle",
|
|
22997
|
+
"taskCompleted",
|
|
22998
|
+
"worktreeCreate",
|
|
22999
|
+
"worktreeRemove"
|
|
23000
|
+
]),
|
|
23001
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
23002
|
+
"command",
|
|
23003
|
+
"prompt",
|
|
23004
|
+
"http",
|
|
23005
|
+
"agent"
|
|
23006
|
+
]),
|
|
23007
|
+
emitsPromptModel: true,
|
|
23008
|
+
stringPassthroughFields: [{
|
|
23009
|
+
canonical: "statusMessage",
|
|
23010
|
+
tool: "statusMessage"
|
|
23011
|
+
}],
|
|
23012
|
+
booleanPassthroughFields: [{
|
|
23013
|
+
canonical: "once",
|
|
23014
|
+
tool: "once"
|
|
23015
|
+
}, {
|
|
23016
|
+
canonical: "async",
|
|
23017
|
+
tool: "async",
|
|
23018
|
+
commandOnly: true
|
|
23019
|
+
}]
|
|
23020
|
+
}
|
|
22919
23021
|
};
|
|
22920
23022
|
/**
|
|
22921
|
-
* Single spelling of the settings/hooks codec/policy: fail closed on an
|
|
22922
|
-
* unparseable root rather than replacing the user's Continue settings with
|
|
22923
|
-
* generated output.
|
|
22924
|
-
*/
|
|
22925
|
-
function parseContinueSettings(fileContent, filePath) {
|
|
22926
|
-
return parseSharedConfig({
|
|
22927
|
-
format: "json",
|
|
22928
|
-
fileContent,
|
|
22929
|
-
filePath,
|
|
22930
|
-
invalidRootPolicy: "error"
|
|
22931
|
-
});
|
|
22932
|
-
}
|
|
22933
|
-
/**
|
|
22934
23023
|
* Continue CLI hooks.
|
|
22935
23024
|
*
|
|
22936
23025
|
* Hooks live under the top-level `hooks` key of `<project>/.continue/settings.json`
|
|
@@ -22946,15 +23035,9 @@ function parseContinueSettings(fileContent, filePath) {
|
|
|
22946
23035
|
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/hookConfig.ts
|
|
22947
23036
|
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/types.ts
|
|
22948
23037
|
*/
|
|
22949
|
-
var ContinueHooks = class
|
|
22950
|
-
|
|
22951
|
-
|
|
22952
|
-
...params,
|
|
22953
|
-
fileContent: params.fileContent ?? "{}"
|
|
22954
|
-
});
|
|
22955
|
-
}
|
|
22956
|
-
isDeletable() {
|
|
22957
|
-
return false;
|
|
23038
|
+
var ContinueHooks = class extends SettingsJsonHooks {
|
|
23039
|
+
static getSpec() {
|
|
23040
|
+
return CONTINUE_SPEC;
|
|
22958
23041
|
}
|
|
22959
23042
|
static getSettablePaths(_options = {}) {
|
|
22960
23043
|
return {
|
|
@@ -22962,77 +23045,6 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22962
23045
|
relativeFilePath: CONTINUE_SETTINGS_FILE_NAME
|
|
22963
23046
|
};
|
|
22964
23047
|
}
|
|
22965
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22966
|
-
const paths = ContinueHooks.getSettablePaths({ global });
|
|
22967
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22968
|
-
return new ContinueHooks({
|
|
22969
|
-
outputRoot,
|
|
22970
|
-
relativeDirPath: paths.relativeDirPath,
|
|
22971
|
-
relativeFilePath: paths.relativeFilePath,
|
|
22972
|
-
fileContent,
|
|
22973
|
-
validate
|
|
22974
|
-
});
|
|
22975
|
-
}
|
|
22976
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
22977
|
-
const paths = ContinueHooks.getSettablePaths({ global });
|
|
22978
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22979
|
-
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22980
|
-
const config = rulesyncHooks.getJson();
|
|
22981
|
-
const hooks = canonicalToToolHooks({
|
|
22982
|
-
config,
|
|
22983
|
-
toolOverrideHooks: config.continue?.hooks,
|
|
22984
|
-
converterConfig: CONTINUE_CONVERTER_CONFIG,
|
|
22985
|
-
logger
|
|
22986
|
-
});
|
|
22987
|
-
const fileContent = applySharedConfigPatch({
|
|
22988
|
-
fileKey: sharedConfigFileKey(paths),
|
|
22989
|
-
feature: "hooks",
|
|
22990
|
-
existingContent,
|
|
22991
|
-
patch: { hooks },
|
|
22992
|
-
filePath,
|
|
22993
|
-
logger
|
|
22994
|
-
});
|
|
22995
|
-
return new ContinueHooks({
|
|
22996
|
-
outputRoot,
|
|
22997
|
-
relativeDirPath: paths.relativeDirPath,
|
|
22998
|
-
relativeFilePath: paths.relativeFilePath,
|
|
22999
|
-
fileContent,
|
|
23000
|
-
validate
|
|
23001
|
-
});
|
|
23002
|
-
}
|
|
23003
|
-
toRulesyncHooks({ logger } = {}) {
|
|
23004
|
-
const configPath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
23005
|
-
let settings;
|
|
23006
|
-
try {
|
|
23007
|
-
settings = parseContinueSettings(this.getFileContent(), configPath);
|
|
23008
|
-
} catch (error) {
|
|
23009
|
-
throw new Error(`Failed to parse Continue hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
23010
|
-
}
|
|
23011
|
-
const hooks = toolHooksToCanonical({
|
|
23012
|
-
logger,
|
|
23013
|
-
hooks: settings.hooks,
|
|
23014
|
-
converterConfig: CONTINUE_CONVERTER_CONFIG
|
|
23015
|
-
});
|
|
23016
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
23017
|
-
hooks,
|
|
23018
|
-
overrideKey: "continue"
|
|
23019
|
-
}), null, 2) });
|
|
23020
|
-
}
|
|
23021
|
-
validate() {
|
|
23022
|
-
return {
|
|
23023
|
-
success: true,
|
|
23024
|
-
error: null
|
|
23025
|
-
};
|
|
23026
|
-
}
|
|
23027
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23028
|
-
return new ContinueHooks({
|
|
23029
|
-
outputRoot,
|
|
23030
|
-
relativeDirPath,
|
|
23031
|
-
relativeFilePath,
|
|
23032
|
-
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
23033
|
-
validate: false
|
|
23034
|
-
});
|
|
23035
|
-
}
|
|
23036
23048
|
};
|
|
23037
23049
|
//#endregion
|
|
23038
23050
|
//#region src/features/hooks/copilot-hooks.ts
|
|
@@ -23623,33 +23635,24 @@ const CORTEXCODE_SKILLS_DIR_PATH = (0, node_path.join)(CORTEXCODE_DIR, "skills")
|
|
|
23623
23635
|
const CORTEXCODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(CORTEXCODE_GLOBAL_DIR_PATH, "skills");
|
|
23624
23636
|
//#endregion
|
|
23625
23637
|
//#region src/features/hooks/cortexcode-hooks.ts
|
|
23626
|
-
const
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
+
const CORTEXCODE_SPEC = {
|
|
23639
|
+
displayName: "Cortex Code",
|
|
23640
|
+
overrideKey: "cortexcode",
|
|
23641
|
+
converterConfig: {
|
|
23642
|
+
supportedEvents: CORTEXCODE_HOOK_EVENTS,
|
|
23643
|
+
canonicalToToolEventNames: CANONICAL_TO_CORTEXCODE_EVENT_NAMES,
|
|
23644
|
+
toolToCanonicalEventNames: CORTEXCODE_TO_CANONICAL_EVENT_NAMES,
|
|
23645
|
+
projectDirVar: "$CORTEX_PROJECT_DIR",
|
|
23646
|
+
prefixDotRelativeCommandsOnly: true,
|
|
23647
|
+
noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
|
|
23648
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
23649
|
+
booleanPassthroughFields: [{
|
|
23650
|
+
canonical: "enabled",
|
|
23651
|
+
tool: "enabled"
|
|
23652
|
+
}]
|
|
23653
|
+
}
|
|
23638
23654
|
};
|
|
23639
23655
|
/**
|
|
23640
|
-
* Single spelling of the settings/hooks codec/policy: fail closed on an
|
|
23641
|
-
* unparseable root rather than replacing the user's Cortex Code settings with
|
|
23642
|
-
* generated output.
|
|
23643
|
-
*/
|
|
23644
|
-
function parseCortexcodeSettings(fileContent, filePath) {
|
|
23645
|
-
return parseSharedConfig({
|
|
23646
|
-
format: "json",
|
|
23647
|
-
fileContent,
|
|
23648
|
-
filePath,
|
|
23649
|
-
invalidRootPolicy: "error"
|
|
23650
|
-
});
|
|
23651
|
-
}
|
|
23652
|
-
/**
|
|
23653
23656
|
* Snowflake Cortex Code hooks.
|
|
23654
23657
|
*
|
|
23655
23658
|
* Hooks live under the top-level `hooks` key of `<project>/.cortex/settings.json`
|
|
@@ -23658,20 +23661,16 @@ function parseCortexcodeSettings(fileContent, filePath) {
|
|
|
23658
23661
|
* "<regex>", "hooks": [{ "type": "command" | "prompt", ..., "timeout"?:
|
|
23659
23662
|
* <seconds>, "enabled"?: <boolean> }] }] }`. The project file also holds
|
|
23660
23663
|
* settings rulesync does not own, so generation merges the `hooks` key into
|
|
23661
|
-
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it
|
|
23664
|
+
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it, and
|
|
23665
|
+
* neither file is removed wholesale (hooks.json sits in the CLI-owned
|
|
23666
|
+
* `~/.snowflake/cortex/` tree).
|
|
23662
23667
|
*
|
|
23663
23668
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/extensibility
|
|
23664
23669
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/settings
|
|
23665
23670
|
*/
|
|
23666
|
-
var CortexcodeHooks = class
|
|
23667
|
-
|
|
23668
|
-
|
|
23669
|
-
...params,
|
|
23670
|
-
fileContent: params.fileContent ?? "{}"
|
|
23671
|
-
});
|
|
23672
|
-
}
|
|
23673
|
-
isDeletable() {
|
|
23674
|
-
return false;
|
|
23671
|
+
var CortexcodeHooks = class extends SettingsJsonHooks {
|
|
23672
|
+
static getSpec() {
|
|
23673
|
+
return CORTEXCODE_SPEC;
|
|
23675
23674
|
}
|
|
23676
23675
|
static getSettablePaths({ global = false } = {}) {
|
|
23677
23676
|
return global ? {
|
|
@@ -23682,60 +23681,338 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23682
23681
|
relativeFilePath: CORTEXCODE_SETTINGS_FILE_NAME
|
|
23683
23682
|
};
|
|
23684
23683
|
}
|
|
23684
|
+
};
|
|
23685
|
+
//#endregion
|
|
23686
|
+
//#region src/constants/crush-paths.ts
|
|
23687
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
23688
|
+
const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
|
|
23689
|
+
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
23690
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
23691
|
+
const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
|
|
23692
|
+
const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
|
|
23693
|
+
const CRUSH_CONFIG_FILE_NAME = "crush.json";
|
|
23694
|
+
const CRUSH_HIDDEN_CONFIG_FILE_NAME = ".crush.json";
|
|
23695
|
+
const CRUSH_PERMISSIONS_KEY = "permissions";
|
|
23696
|
+
const CRUSH_ALLOWED_TOOLS_KEY = "allowed_tools";
|
|
23697
|
+
const CRUSH_OPTIONS_KEY = "options";
|
|
23698
|
+
const CRUSH_DISABLED_TOOLS_KEY = "disabled_tools";
|
|
23699
|
+
const CRUSH_HOOKS_KEY = "hooks";
|
|
23700
|
+
//#endregion
|
|
23701
|
+
//#region src/features/crush-config.ts
|
|
23702
|
+
/**
|
|
23703
|
+
* Where Crush's JSON config lives for a scope, before the project-scope twin
|
|
23704
|
+
* is resolved: `<project>/crush.json` or `~/.config/crush/crush.json`. This is
|
|
23705
|
+
* also the key every adapter reports through `getSettablePaths`, so the
|
|
23706
|
+
* shared-config ownership declaration, the gitignore derivation and the
|
|
23707
|
+
* shared-write ordering all see one file per scope.
|
|
23708
|
+
*/
|
|
23709
|
+
function getCrushConfigSettablePaths({ global = false } = {}) {
|
|
23710
|
+
return {
|
|
23711
|
+
relativeDirPath: global ? CRUSH_GLOBAL_DIR : ".",
|
|
23712
|
+
relativeFilePath: CRUSH_CONFIG_FILE_NAME
|
|
23713
|
+
};
|
|
23714
|
+
}
|
|
23715
|
+
/**
|
|
23716
|
+
* Resolves which Crush config file an adapter should read and write.
|
|
23717
|
+
*
|
|
23718
|
+
* At project scope Crush discovers both `.crush.json` and `crush.json` in the
|
|
23719
|
+
* working directory and merges every file it finds with `.crush.json` on top
|
|
23720
|
+
* (objects merge recursively, lists are concatenated, scalars are overridden).
|
|
23721
|
+
* Neither file therefore hides the other: an entry left in `crush.json` stays
|
|
23722
|
+
* in effect next to whatever `.crush.json` says. An existing `.crush.json` is
|
|
23723
|
+
* preferred because the user chose that spelling; otherwise `crush.json` is
|
|
23724
|
+
* used (and created when neither exists). The global scope has a single
|
|
23725
|
+
* spelling.
|
|
23726
|
+
*
|
|
23727
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
23728
|
+
*/
|
|
23729
|
+
async function resolveCrushConfigFile({ outputRoot, global = false }) {
|
|
23730
|
+
const paths = getCrushConfigSettablePaths({ global });
|
|
23731
|
+
const configDir = (0, node_path.join)(outputRoot, paths.relativeDirPath);
|
|
23732
|
+
const filePath = (0, node_path.join)(configDir, paths.relativeFilePath);
|
|
23733
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
23734
|
+
if (!global) {
|
|
23735
|
+
const hiddenPath = (0, node_path.join)(configDir, CRUSH_HIDDEN_CONFIG_FILE_NAME);
|
|
23736
|
+
const hiddenContent = await readFileContentOrNull(hiddenPath);
|
|
23737
|
+
if (hiddenContent !== null) return {
|
|
23738
|
+
relativeDirPath: paths.relativeDirPath,
|
|
23739
|
+
relativeFilePath: CRUSH_HIDDEN_CONFIG_FILE_NAME,
|
|
23740
|
+
filePath: hiddenPath,
|
|
23741
|
+
fileContent: hiddenContent,
|
|
23742
|
+
...fileContent === null ? {} : { twin: {
|
|
23743
|
+
filePath,
|
|
23744
|
+
fileContent
|
|
23745
|
+
} }
|
|
23746
|
+
};
|
|
23747
|
+
}
|
|
23748
|
+
return {
|
|
23749
|
+
relativeDirPath: paths.relativeDirPath,
|
|
23750
|
+
relativeFilePath: paths.relativeFilePath,
|
|
23751
|
+
filePath,
|
|
23752
|
+
fileContent
|
|
23753
|
+
};
|
|
23754
|
+
}
|
|
23755
|
+
/**
|
|
23756
|
+
* Single spelling of the crush.json codec/policy, matching the
|
|
23757
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
23758
|
+
* unparseable root rather than replacing the user's Crush config with
|
|
23759
|
+
* generated output.
|
|
23760
|
+
*/
|
|
23761
|
+
function parseCrushConfig(fileContent, filePath) {
|
|
23762
|
+
return parseSharedConfig({
|
|
23763
|
+
format: "json",
|
|
23764
|
+
fileContent,
|
|
23765
|
+
filePath,
|
|
23766
|
+
invalidRootPolicy: "error"
|
|
23767
|
+
});
|
|
23768
|
+
}
|
|
23769
|
+
/**
|
|
23770
|
+
* Combine two Crush config documents the way Crush itself does
|
|
23771
|
+
* (`github.com/qjebbs/go-jsons`): objects merge recursively, arrays are
|
|
23772
|
+
* concatenated with `base` first, and any other value from `override` wins.
|
|
23773
|
+
*/
|
|
23774
|
+
function mergeCrushConfigs({ base, override }) {
|
|
23775
|
+
const merged = { ...base };
|
|
23776
|
+
for (const [key, value] of Object.entries(override)) {
|
|
23777
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
23778
|
+
const current = merged[key];
|
|
23779
|
+
if (isRecord$1(current) && isRecord$1(value)) merged[key] = mergeCrushConfigs({
|
|
23780
|
+
base: current,
|
|
23781
|
+
override: value
|
|
23782
|
+
});
|
|
23783
|
+
else if (Array.isArray(current) && Array.isArray(value)) merged[key] = [...current, ...value];
|
|
23784
|
+
else merged[key] = value;
|
|
23785
|
+
}
|
|
23786
|
+
return merged;
|
|
23787
|
+
}
|
|
23788
|
+
/**
|
|
23789
|
+
* The content an import should read for a resolved location: the chosen file
|
|
23790
|
+
* merged over its twin when both exist, so a server, hook or tool entry that
|
|
23791
|
+
* only the lower-priority `crush.json` declares is imported too — Crush sees
|
|
23792
|
+
* it, so rulesync should as well.
|
|
23793
|
+
*/
|
|
23794
|
+
function crushConfigImportContent(location) {
|
|
23795
|
+
const own = location.fileContent ?? "{}";
|
|
23796
|
+
if (location.twin === void 0) return own;
|
|
23797
|
+
const merged = mergeCrushConfigs({
|
|
23798
|
+
base: parseCrushConfig(location.twin.fileContent, location.twin.filePath),
|
|
23799
|
+
override: parseCrushConfig(own, location.filePath)
|
|
23800
|
+
});
|
|
23801
|
+
return JSON.stringify(merged, null, 2);
|
|
23802
|
+
}
|
|
23803
|
+
function lookupPath(root, path) {
|
|
23804
|
+
let current = root;
|
|
23805
|
+
for (const segment of path) {
|
|
23806
|
+
if (!isRecord$1(current) || !Object.hasOwn(current, segment)) return;
|
|
23807
|
+
current = current[segment];
|
|
23808
|
+
}
|
|
23809
|
+
return current;
|
|
23810
|
+
}
|
|
23811
|
+
function isNonEmptyValue(value) {
|
|
23812
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
23813
|
+
if (isRecord$1(value)) return Object.keys(value).length > 0;
|
|
23814
|
+
return value !== void 0;
|
|
23815
|
+
}
|
|
23816
|
+
/**
|
|
23817
|
+
* Warn when the twin Crush merges beneath the written file still carries a
|
|
23818
|
+
* value at one of the paths this feature owns. Rulesync only rewrites the
|
|
23819
|
+
* chosen file, and because Crush concatenates lists and merges objects across
|
|
23820
|
+
* the pair, such an entry — typically one an earlier generate wrote to
|
|
23821
|
+
* `crush.json` before the user added a `.crush.json` — stays in effect until
|
|
23822
|
+
* removed by hand, and cannot be retracted from `.rulesync/`.
|
|
23823
|
+
*/
|
|
23824
|
+
function warnCrushTwinLeftovers({ location, ownedPaths, logger }) {
|
|
23825
|
+
if (location.twin === void 0) return;
|
|
23826
|
+
let twin;
|
|
23827
|
+
try {
|
|
23828
|
+
twin = parseCrushConfig(location.twin.fileContent, location.twin.filePath);
|
|
23829
|
+
} catch {
|
|
23830
|
+
return;
|
|
23831
|
+
}
|
|
23832
|
+
const leftovers = ownedPaths.filter((path) => isNonEmptyValue(lookupPath(twin, path))).map((path) => `"${path.join(".")}"`);
|
|
23833
|
+
if (leftovers.length === 0) return;
|
|
23834
|
+
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.`);
|
|
23835
|
+
}
|
|
23836
|
+
//#endregion
|
|
23837
|
+
//#region src/features/hooks/crush-hooks.ts
|
|
23838
|
+
const SUPPORTED_CRUSH_EVENTS = new Set(CRUSH_HOOK_EVENTS);
|
|
23839
|
+
/**
|
|
23840
|
+
* Crush matches an event key case-insensitively and ignores underscores
|
|
23841
|
+
* (`PreToolUse`, `pre_tool_use` and `PRE_TOOL_USE` all work), so an import
|
|
23842
|
+
* looks the canonical name up by that normalized spelling.
|
|
23843
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
23844
|
+
*/
|
|
23845
|
+
const NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CRUSH_TO_CANONICAL_EVENT_NAMES).map(([crushEvent, canonical]) => [normalizeCrushEventName(crushEvent), canonical]));
|
|
23846
|
+
function normalizeCrushEventName(event) {
|
|
23847
|
+
return event.replaceAll("_", "").toLowerCase();
|
|
23848
|
+
}
|
|
23849
|
+
/**
|
|
23850
|
+
* Build the `hooks` block of `crush.json` from a canonical hooks config.
|
|
23851
|
+
* Crush keys a flat array of `{name, matcher, command, timeout}` entries by
|
|
23852
|
+
* event name; `matcher` is a regex tested against the (lower-case) Crush tool
|
|
23853
|
+
* name and `timeout` is in seconds. Only `type: "command"` canonical hooks are
|
|
23854
|
+
* emitted, since a Crush hook is a shell command. A shared canonical event
|
|
23855
|
+
* Crush does not fire is skipped (the HooksProcessor reports it), while an
|
|
23856
|
+
* event under the `crush.hooks` override — such as one an import filed there
|
|
23857
|
+
* — is written verbatim so it round-trips.
|
|
23858
|
+
*/
|
|
23859
|
+
function canonicalToCrushHooks({ config, toolOverride, logger }) {
|
|
23860
|
+
const sharedHooks = {};
|
|
23861
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_CRUSH_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
23862
|
+
const effective = {
|
|
23863
|
+
...sharedHooks,
|
|
23864
|
+
...toolOverride
|
|
23865
|
+
};
|
|
23866
|
+
const hooks = {};
|
|
23867
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
23868
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
23869
|
+
const crushEvent = lookupOwn({
|
|
23870
|
+
record: CANONICAL_TO_CRUSH_EVENT_NAMES,
|
|
23871
|
+
key: event
|
|
23872
|
+
}) ?? event;
|
|
23873
|
+
const entries = [];
|
|
23874
|
+
for (const def of defs) {
|
|
23875
|
+
const entry = canonicalDefToCrushEntry({
|
|
23876
|
+
def,
|
|
23877
|
+
event,
|
|
23878
|
+
logger
|
|
23879
|
+
});
|
|
23880
|
+
if (entry !== null) entries.push(entry);
|
|
23881
|
+
}
|
|
23882
|
+
if (entries.length > 0) hooks[crushEvent] = entries;
|
|
23883
|
+
}
|
|
23884
|
+
return hooks;
|
|
23885
|
+
}
|
|
23886
|
+
/** Convert one canonical hook definition to a Crush entry, or null to skip. */
|
|
23887
|
+
function canonicalDefToCrushEntry({ def, event, logger }) {
|
|
23888
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
23889
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
23890
|
+
logger?.warn(`Crush hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Crush would discard, so it was skipped.`);
|
|
23891
|
+
return null;
|
|
23892
|
+
}
|
|
23893
|
+
const entry = { command: def.command };
|
|
23894
|
+
if (typeof def.name === "string" && def.name !== "") entry.name = def.name;
|
|
23895
|
+
if (typeof def.matcher === "string" && def.matcher !== "" && def.matcher !== "*") entry.matcher = def.matcher;
|
|
23896
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
23897
|
+
return entry;
|
|
23898
|
+
}
|
|
23899
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
23900
|
+
function crushEntryToCanonicalDef(raw) {
|
|
23901
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string") return null;
|
|
23902
|
+
const def = {
|
|
23903
|
+
type: "command",
|
|
23904
|
+
command: raw.command
|
|
23905
|
+
};
|
|
23906
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
23907
|
+
if (typeof raw.matcher === "string" && raw.matcher !== "") def.matcher = raw.matcher;
|
|
23908
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
23909
|
+
return def;
|
|
23910
|
+
}
|
|
23911
|
+
/**
|
|
23912
|
+
* Reverse {@link canonicalToCrushHooks}: parse the `hooks` block back into a
|
|
23913
|
+
* canonical event → definition[] record. An event Crush does not document is
|
|
23914
|
+
* carried under its own name so `buildImportedHooksConfig` files it under the
|
|
23915
|
+
* `crush.hooks` override.
|
|
23916
|
+
*/
|
|
23917
|
+
function crushHooksToCanonical(hooksBlock) {
|
|
23918
|
+
const canonical = {};
|
|
23919
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
23920
|
+
for (const [crushEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
23921
|
+
if (isPrototypePollutionKey(crushEvent) || !Array.isArray(rawEntries)) continue;
|
|
23922
|
+
const canonicalEvent = lookupOwn({
|
|
23923
|
+
record: NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES,
|
|
23924
|
+
key: normalizeCrushEventName(crushEvent)
|
|
23925
|
+
}) ?? crushEvent;
|
|
23926
|
+
const defs = rawEntries.map((raw) => crushEntryToCanonicalDef(raw)).filter((def) => def !== null);
|
|
23927
|
+
if (defs.length === 0) continue;
|
|
23928
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
23929
|
+
record: canonical,
|
|
23930
|
+
key: canonicalEvent
|
|
23931
|
+
}) ?? [], ...defs];
|
|
23932
|
+
}
|
|
23933
|
+
return canonical;
|
|
23934
|
+
}
|
|
23935
|
+
/**
|
|
23936
|
+
* Crush hooks.
|
|
23937
|
+
*
|
|
23938
|
+
* Crush reads hooks from the `hooks` key of its JSON config —
|
|
23939
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
23940
|
+
* lists concatenated) at project scope and `~/.config/crush/crush.json` at
|
|
23941
|
+
* user scope — as
|
|
23942
|
+
* `hooks.<Event>: [{name?, matcher?, command, timeout?}]`. Only `PreToolUse`
|
|
23943
|
+
* fires today. The `hooks` key is owned outright; every other top-level key
|
|
23944
|
+
* of the file is preserved and the file is never deleted.
|
|
23945
|
+
*
|
|
23946
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
23947
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
23948
|
+
*/
|
|
23949
|
+
var CrushHooks = class CrushHooks extends ToolHooks {
|
|
23950
|
+
json;
|
|
23951
|
+
constructor(params) {
|
|
23952
|
+
super(params);
|
|
23953
|
+
this.json = parseCrushConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
23954
|
+
}
|
|
23955
|
+
getJson() {
|
|
23956
|
+
return this.json;
|
|
23957
|
+
}
|
|
23958
|
+
isDeletable() {
|
|
23959
|
+
return false;
|
|
23960
|
+
}
|
|
23961
|
+
static getSettablePaths({ global = false } = {}) {
|
|
23962
|
+
return getCrushConfigSettablePaths({ global });
|
|
23963
|
+
}
|
|
23685
23964
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
23686
|
-
const
|
|
23687
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
23688
|
-
return new CortexcodeHooks({
|
|
23965
|
+
const location = await resolveCrushConfigFile({
|
|
23689
23966
|
outputRoot,
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23967
|
+
global
|
|
23968
|
+
});
|
|
23969
|
+
return new CrushHooks({
|
|
23970
|
+
outputRoot,
|
|
23971
|
+
relativeDirPath: location.relativeDirPath,
|
|
23972
|
+
relativeFilePath: location.relativeFilePath,
|
|
23973
|
+
fileContent: crushConfigImportContent(location),
|
|
23974
|
+
validate,
|
|
23975
|
+
global
|
|
23694
23976
|
});
|
|
23695
23977
|
}
|
|
23696
23978
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
23697
|
-
const
|
|
23698
|
-
|
|
23699
|
-
|
|
23700
|
-
|
|
23701
|
-
const
|
|
23702
|
-
|
|
23703
|
-
|
|
23704
|
-
|
|
23979
|
+
const location = await resolveCrushConfigFile({
|
|
23980
|
+
outputRoot,
|
|
23981
|
+
global
|
|
23982
|
+
});
|
|
23983
|
+
const existingContent = location.fileContent ?? "";
|
|
23984
|
+
warnCrushTwinLeftovers({
|
|
23985
|
+
location,
|
|
23986
|
+
ownedPaths: [[CRUSH_HOOKS_KEY]],
|
|
23705
23987
|
logger
|
|
23706
23988
|
});
|
|
23707
|
-
const
|
|
23708
|
-
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
patch: { hooks },
|
|
23712
|
-
filePath,
|
|
23989
|
+
const config = rulesyncHooks.getJson();
|
|
23990
|
+
const hooks = canonicalToCrushHooks({
|
|
23991
|
+
config,
|
|
23992
|
+
toolOverride: config.crush?.hooks,
|
|
23713
23993
|
logger
|
|
23714
23994
|
});
|
|
23715
|
-
return new
|
|
23995
|
+
return new CrushHooks({
|
|
23716
23996
|
outputRoot,
|
|
23717
|
-
relativeDirPath:
|
|
23718
|
-
relativeFilePath:
|
|
23719
|
-
fileContent
|
|
23720
|
-
|
|
23997
|
+
relativeDirPath: location.relativeDirPath,
|
|
23998
|
+
relativeFilePath: location.relativeFilePath,
|
|
23999
|
+
fileContent: applySharedConfigPatch({
|
|
24000
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
24001
|
+
feature: "hooks",
|
|
24002
|
+
existingContent,
|
|
24003
|
+
patch: { [CRUSH_HOOKS_KEY]: hooks },
|
|
24004
|
+
filePath: location.filePath,
|
|
24005
|
+
logger
|
|
24006
|
+
}),
|
|
24007
|
+
validate,
|
|
24008
|
+
global
|
|
23721
24009
|
});
|
|
23722
24010
|
}
|
|
23723
|
-
toRulesyncHooks(
|
|
23724
|
-
const
|
|
23725
|
-
let settings;
|
|
23726
|
-
try {
|
|
23727
|
-
settings = parseCortexcodeSettings(this.getFileContent(), configPath);
|
|
23728
|
-
} catch (error) {
|
|
23729
|
-
throw new Error(`Failed to parse Cortex Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
23730
|
-
}
|
|
23731
|
-
const hooks = toolHooksToCanonical({
|
|
23732
|
-
logger,
|
|
23733
|
-
hooks: settings.hooks,
|
|
23734
|
-
converterConfig: CORTEXCODE_CONVERTER_CONFIG
|
|
23735
|
-
});
|
|
24011
|
+
toRulesyncHooks() {
|
|
24012
|
+
const hooks = crushHooksToCanonical(this.json[CRUSH_HOOKS_KEY]);
|
|
23736
24013
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
23737
24014
|
hooks,
|
|
23738
|
-
overrideKey: "
|
|
24015
|
+
overrideKey: "crush"
|
|
23739
24016
|
}), null, 2) });
|
|
23740
24017
|
}
|
|
23741
24018
|
validate() {
|
|
@@ -23744,13 +24021,14 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23744
24021
|
error: null
|
|
23745
24022
|
};
|
|
23746
24023
|
}
|
|
23747
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23748
|
-
return new
|
|
24024
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
24025
|
+
return new CrushHooks({
|
|
23749
24026
|
outputRoot,
|
|
23750
24027
|
relativeDirPath,
|
|
23751
24028
|
relativeFilePath,
|
|
23752
|
-
fileContent: JSON.stringify({
|
|
23753
|
-
validate: false
|
|
24029
|
+
fileContent: JSON.stringify({ [CRUSH_HOOKS_KEY]: {} }, null, 2),
|
|
24030
|
+
validate: false,
|
|
24031
|
+
global
|
|
23754
24032
|
});
|
|
23755
24033
|
}
|
|
23756
24034
|
};
|
|
@@ -26984,19 +27262,22 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
|
26984
27262
|
//#endregion
|
|
26985
27263
|
//#region src/features/hooks/tabnine-hooks.ts
|
|
26986
27264
|
/**
|
|
26987
|
-
* Environment block safe to hand Tabnine for a hook.
|
|
26988
|
-
*
|
|
26989
|
-
*
|
|
26990
|
-
*
|
|
26991
|
-
*
|
|
27265
|
+
* Environment block safe to hand Tabnine for a hook. The shared converter
|
|
27266
|
+
* refuses a whole `env` map holding one bad entry; Tabnine's shape is authored
|
|
27267
|
+
* outside it, so the same per-entry rule (`isSafeEnvEntry`: a key holding `=`,
|
|
27268
|
+
* a control character or nothing at all names a different variable than it
|
|
27269
|
+
* appears to) is applied entry by entry and the bad ones are dropped in both
|
|
27270
|
+
* directions, with a warning on export, where the value came from an authored
|
|
27271
|
+
* `.rulesync/hooks.*`.
|
|
26992
27272
|
*/
|
|
26993
27273
|
function sanitizeEnv({ env, warn }) {
|
|
26994
27274
|
if (env === null || env === void 0 || typeof env !== "object" || Array.isArray(env)) return;
|
|
26995
27275
|
const result = {};
|
|
26996
27276
|
for (const [key, value] of Object.entries(env)) {
|
|
26997
|
-
|
|
26998
|
-
|
|
26999
|
-
|
|
27277
|
+
if (!isSafeEnvEntry({
|
|
27278
|
+
key,
|
|
27279
|
+
value
|
|
27280
|
+
})) {
|
|
27000
27281
|
warn?.(`Tabnine CLI hook env entry ${JSON.stringify(key)} is not a safe KEY=VALUE pair; skipping it.`);
|
|
27001
27282
|
continue;
|
|
27002
27283
|
}
|
|
@@ -27097,7 +27378,7 @@ const TabnineMatcherEntrySchema = zod_mini.z.looseObject({
|
|
|
27097
27378
|
});
|
|
27098
27379
|
function tabnineMatcherEntryToCanonical(entry) {
|
|
27099
27380
|
const sequential = entry.sequential === true;
|
|
27100
|
-
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
27381
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && entry.matcher !== ".*" ? entry.matcher : void 0;
|
|
27101
27382
|
const defs = [];
|
|
27102
27383
|
for (const hook of entry.hooks ?? []) {
|
|
27103
27384
|
if (hook.type !== "command" || typeof hook.command !== "string" || hook.command === "") continue;
|
|
@@ -28139,6 +28420,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28139
28420
|
supportedHookTypes: ["command", "http"],
|
|
28140
28421
|
supportsMatcher: true
|
|
28141
28422
|
}],
|
|
28423
|
+
["crush", {
|
|
28424
|
+
class: CrushHooks,
|
|
28425
|
+
meta: {
|
|
28426
|
+
supportsProject: true,
|
|
28427
|
+
supportsGlobal: true,
|
|
28428
|
+
supportsImport: true
|
|
28429
|
+
},
|
|
28430
|
+
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
28431
|
+
supportedHookTypes: ["command"],
|
|
28432
|
+
supportsMatcher: true,
|
|
28433
|
+
passthroughOverrideEvents: true
|
|
28434
|
+
}],
|
|
28142
28435
|
["zcode", {
|
|
28143
28436
|
class: ZcodeHooks,
|
|
28144
28437
|
meta: {
|
|
@@ -28820,14 +29113,6 @@ var ContinueIgnore = class ContinueIgnore extends ToolIgnore {
|
|
|
28820
29113
|
}
|
|
28821
29114
|
};
|
|
28822
29115
|
//#endregion
|
|
28823
|
-
//#region src/constants/crush-paths.ts
|
|
28824
|
-
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
28825
|
-
const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
|
|
28826
|
-
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
28827
|
-
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
28828
|
-
const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
|
|
28829
|
-
const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
|
|
28830
|
-
//#endregion
|
|
28831
29116
|
//#region src/features/ignore/crush-ignore.ts
|
|
28832
29117
|
/**
|
|
28833
29118
|
* Ignore generator for Crush.
|
|
@@ -30714,13 +30999,17 @@ function isRemoteMcpServer(serverConfig) {
|
|
|
30714
30999
|
function resolveRemoteMcpUrl(serverConfig) {
|
|
30715
31000
|
return serverConfig.url || serverConfig.httpUrl || void 0;
|
|
30716
31001
|
}
|
|
30717
|
-
/**
|
|
31002
|
+
/**
|
|
31003
|
+
* The `command` array a `local` server is spawned with, `args` merged in. An
|
|
31004
|
+
* entry carrying `args` but no `command` comes back empty rather than with
|
|
31005
|
+
* `args[0]` promoted to its program: every adapter writes element 0 back out
|
|
31006
|
+
* as the executable (or the whole array as the command line), so the caller
|
|
31007
|
+
* skips such an entry the same way as one with nothing to spawn at all.
|
|
31008
|
+
*/
|
|
30718
31009
|
function resolveLocalMcpCommand(serverConfig) {
|
|
30719
|
-
const
|
|
30720
|
-
|
|
30721
|
-
|
|
30722
|
-
else commandArray.push(serverConfig.command);
|
|
30723
|
-
}
|
|
31010
|
+
const { command } = serverConfig;
|
|
31011
|
+
const commandArray = Array.isArray(command) ? [...command] : command ? [command] : [];
|
|
31012
|
+
if (commandArray.length === 0) return [];
|
|
30724
31013
|
if (serverConfig.args) commandArray.push(...serverConfig.args);
|
|
30725
31014
|
return commandArray;
|
|
30726
31015
|
}
|
|
@@ -30730,9 +31019,13 @@ function resolveLocalMcpCommand(serverConfig) {
|
|
|
30730
31019
|
* spells the transport out. Writing `{type: "local", command: []}` for it would
|
|
30731
31020
|
* give the tool a server it cannot start and the importer a file it cannot read
|
|
30732
31021
|
* back, so it is skipped out loud instead.
|
|
31022
|
+
*
|
|
31023
|
+
* The server name comes straight from the user's config, so it is quoted
|
|
31024
|
+
* through `quoteValueForWarning` rather than interpolated: a crafted name could
|
|
31025
|
+
* otherwise carry a line break or control characters into the log.
|
|
30733
31026
|
*/
|
|
30734
31027
|
function warnAndSkipMcpServer({ toolName, serverName, reason, logger }) {
|
|
30735
|
-
logger?.warn(`${toolName} MCP: skipping
|
|
31028
|
+
logger?.warn(`${toolName} MCP: skipping ${quoteValueForWarning(serverName)} because it declares ${reason}.`);
|
|
30736
31029
|
return null;
|
|
30737
31030
|
}
|
|
30738
31031
|
/**
|
|
@@ -30820,8 +31113,10 @@ function asBobRemoteType(stated, url) {
|
|
|
30820
31113
|
* streamable HTTP server carries `type: "streamable-http"` and `url`, and an
|
|
30821
31114
|
* SSE server carries a bare `url`. The canonical `transport` alias and the
|
|
30822
31115
|
* Claude-style `httpUrl` alias are folded into `type`/`url`; `env`, `cwd`,
|
|
30823
|
-
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through
|
|
30824
|
-
*
|
|
31116
|
+
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through, as Bob
|
|
31117
|
+
* documents all of them, with prototype-pollution keys dropped at every
|
|
31118
|
+
* nesting level (Bob spreads `env` and `headers` into the process environment
|
|
31119
|
+
* and the HTTP requests).
|
|
30825
31120
|
*
|
|
30826
31121
|
* Bob Shell documents the same file with an `httpURL` key instead of
|
|
30827
31122
|
* `type` + `url` for streamable HTTP. rulesync writes the IDE spelling (the two
|
|
@@ -30889,7 +31184,7 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30889
31184
|
}
|
|
30890
31185
|
for (const [key, value] of Object.entries(rest)) {
|
|
30891
31186
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30892
|
-
converted[key] = (
|
|
31187
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30893
31188
|
}
|
|
30894
31189
|
result[serverName] = converted;
|
|
30895
31190
|
}
|
|
@@ -30900,7 +31195,9 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30900
31195
|
* (`type: "streamable-http"` + `url`) is already canonical and passes through;
|
|
30901
31196
|
* the Bob Shell spelling `httpURL` becomes `url` with `type: "http"`; a bare
|
|
30902
31197
|
* `url` is an SSE server in Bob, so it gains `type: "sse"` to keep that reading
|
|
30903
|
-
* on the next generate.
|
|
31198
|
+
* on the next generate. Every other field passes through with its
|
|
31199
|
+
* prototype-pollution keys dropped at every nesting level, mirroring the
|
|
31200
|
+
* generate side's handling of `env` / `headers`.
|
|
30904
31201
|
*/
|
|
30905
31202
|
function convertFromBobFormat(mcpServers) {
|
|
30906
31203
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -30915,7 +31212,7 @@ function convertFromBobFormat(mcpServers) {
|
|
|
30915
31212
|
if (typeof value === "string") httpURL = value;
|
|
30916
31213
|
continue;
|
|
30917
31214
|
}
|
|
30918
|
-
converted[key] = value;
|
|
31215
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30919
31216
|
}
|
|
30920
31217
|
if (httpURL !== void 0) {
|
|
30921
31218
|
converted.url = httpURL;
|
|
@@ -31763,7 +32060,7 @@ function convertRemoteServer({ serverName, serverConfig, logger }) {
|
|
|
31763
32060
|
return converted;
|
|
31764
32061
|
}
|
|
31765
32062
|
function convertStdioServer({ serverName, serverConfig, logger }) {
|
|
31766
|
-
const [command, ...args] =
|
|
32063
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
31767
32064
|
if (!command) {
|
|
31768
32065
|
warnAndSkipMcpServer({
|
|
31769
32066
|
toolName: "Continue",
|
|
@@ -31779,7 +32076,7 @@ function convertStdioServer({ serverName, serverConfig, logger }) {
|
|
|
31779
32076
|
};
|
|
31780
32077
|
if (args.length > 0) converted.args = args;
|
|
31781
32078
|
if (isRecord$1(serverConfig.env)) converted.env = omitPrototypePollutionKeys(serverConfig.env);
|
|
31782
|
-
if (typeof serverConfig.envFile === "string") logger?.warn(`Continue ignores "envFile" for MCP servers, so the envFile of server
|
|
32079
|
+
if (typeof serverConfig.envFile === "string") logger?.warn(`Continue ignores "envFile" for MCP servers, so the envFile of server ${quoteValueForWarning(serverName)} was not written; put the variables in "env" instead.`);
|
|
31783
32080
|
return converted;
|
|
31784
32081
|
}
|
|
31785
32082
|
/**
|
|
@@ -32321,7 +32618,7 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32321
32618
|
}
|
|
32322
32619
|
for (const [key, value] of Object.entries(rest)) {
|
|
32323
32620
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32324
|
-
converted[key] = (
|
|
32621
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
32325
32622
|
}
|
|
32326
32623
|
result[serverName] = converted;
|
|
32327
32624
|
}
|
|
@@ -32330,14 +32627,15 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32330
32627
|
/**
|
|
32331
32628
|
* Convert Cortex Code's server map back to the canonical shape. The
|
|
32332
32629
|
* documented spelling (`type` + `command`/`url`) is already canonical, so
|
|
32333
|
-
* entries pass through with only prototype-pollution keys dropped
|
|
32630
|
+
* entries pass through with only prototype-pollution keys dropped — at every
|
|
32631
|
+
* nesting level, mirroring the generate side.
|
|
32334
32632
|
*/
|
|
32335
32633
|
function convertFromCortexcodeFormat(mcpServers) {
|
|
32336
32634
|
if (!isMcpServers(mcpServers)) return {};
|
|
32337
32635
|
const result = {};
|
|
32338
32636
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32339
32637
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32340
|
-
result[serverName] =
|
|
32638
|
+
result[serverName] = omitPrototypePollutionKeysDeep(serverConfig);
|
|
32341
32639
|
}
|
|
32342
32640
|
return result;
|
|
32343
32641
|
}
|
|
@@ -32376,19 +32674,12 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32376
32674
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32377
32675
|
if (!global) throw new Error(CORTEXCODE_GLOBAL_ONLY_MESSAGE);
|
|
32378
32676
|
const paths = this.getSettablePaths({ global });
|
|
32379
|
-
const
|
|
32380
|
-
fileContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32381
|
-
relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
|
|
32382
|
-
});
|
|
32383
|
-
const newJson = {
|
|
32384
|
-
...json,
|
|
32385
|
-
mcpServers: json.mcpServers ?? {}
|
|
32386
|
-
};
|
|
32677
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32387
32678
|
return new CortexcodeMcp({
|
|
32388
32679
|
outputRoot,
|
|
32389
32680
|
relativeDirPath: paths.relativeDirPath,
|
|
32390
32681
|
relativeFilePath: paths.relativeFilePath,
|
|
32391
|
-
fileContent
|
|
32682
|
+
fileContent,
|
|
32392
32683
|
validate,
|
|
32393
32684
|
global
|
|
32394
32685
|
});
|
|
@@ -32436,6 +32727,319 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32436
32727
|
}
|
|
32437
32728
|
};
|
|
32438
32729
|
//#endregion
|
|
32730
|
+
//#region src/features/mcp/crush-mcp.ts
|
|
32731
|
+
/**
|
|
32732
|
+
* Crush-only MCP fields with no canonical counterpart, authored under a
|
|
32733
|
+
* `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
|
|
32734
|
+
* name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers and
|
|
32735
|
+
* `sessionless` marks a server that issues no `Mcp-Session-Id`.
|
|
32736
|
+
* `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
|
|
32737
|
+
* never reaches another tool's config; `fromRulesyncMcp` re-merges them from
|
|
32738
|
+
* the raw source JSON, honouring the raw Crush spelling as a fallback for an
|
|
32739
|
+
* entry copied straight out of a `crush.json` (the `experimental_environment`
|
|
32740
|
+
* precedent) — except `oauth`, whose canonical key is Claude Code's
|
|
32741
|
+
* `{ clientId }` object and which only `crushOauth` can set.
|
|
32742
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
32743
|
+
*/
|
|
32744
|
+
const CRUSH_ONLY_KEYS = [
|
|
32745
|
+
{
|
|
32746
|
+
canonical: "crushOauth",
|
|
32747
|
+
crush: "oauth",
|
|
32748
|
+
accepts: (v) => typeof v === "boolean",
|
|
32749
|
+
rawFallback: false
|
|
32750
|
+
},
|
|
32751
|
+
{
|
|
32752
|
+
canonical: "crushOauthClientId",
|
|
32753
|
+
crush: "oauth_client_id",
|
|
32754
|
+
accepts: (v) => typeof v === "string",
|
|
32755
|
+
rawFallback: true
|
|
32756
|
+
},
|
|
32757
|
+
{
|
|
32758
|
+
canonical: "crushOauthClientSecret",
|
|
32759
|
+
crush: "oauth_client_secret",
|
|
32760
|
+
accepts: (v) => typeof v === "string",
|
|
32761
|
+
rawFallback: true
|
|
32762
|
+
},
|
|
32763
|
+
{
|
|
32764
|
+
canonical: "crushOauthCallbackPort",
|
|
32765
|
+
crush: "oauth_callback_port",
|
|
32766
|
+
accepts: (v) => Number.isInteger(v),
|
|
32767
|
+
rawFallback: true
|
|
32768
|
+
},
|
|
32769
|
+
{
|
|
32770
|
+
canonical: "crushSessionless",
|
|
32771
|
+
crush: "sessionless",
|
|
32772
|
+
accepts: (v) => typeof v === "boolean",
|
|
32773
|
+
rawFallback: true
|
|
32774
|
+
}
|
|
32775
|
+
];
|
|
32776
|
+
/**
|
|
32777
|
+
* The Crush-only keys of one raw canonical server entry, keyed by their
|
|
32778
|
+
* canonical name, so `convertToCrushFormat` sees them after the shared map
|
|
32779
|
+
* stripped them.
|
|
32780
|
+
*/
|
|
32781
|
+
function readCrushOnlyKeys(rawServer) {
|
|
32782
|
+
const result = {};
|
|
32783
|
+
if (!isRecord$1(rawServer)) return result;
|
|
32784
|
+
for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
|
|
32785
|
+
const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
|
|
32786
|
+
if (value !== void 0) result[canonical] = value;
|
|
32787
|
+
}
|
|
32788
|
+
return result;
|
|
32789
|
+
}
|
|
32790
|
+
function copyCrushOnlyKeys({ from, to }) {
|
|
32791
|
+
for (const { canonical, crush, accepts } of CRUSH_ONLY_KEYS) if (accepts(from[canonical])) to[crush] = from[canonical];
|
|
32792
|
+
}
|
|
32793
|
+
/**
|
|
32794
|
+
* The remote transports Crush's `MCPType` enum accepts, spelled the way it
|
|
32795
|
+
* reads them. `streamable-http` is the canonical rulesync name for what Crush
|
|
32796
|
+
* calls `http`; a WebSocket server has no Crush transport and is left to the
|
|
32797
|
+
* caller to reject.
|
|
32798
|
+
*/
|
|
32799
|
+
function asCrushRemoteType(stated, url) {
|
|
32800
|
+
if (stated === "sse") return "sse";
|
|
32801
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
32802
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
32803
|
+
}
|
|
32804
|
+
/**
|
|
32805
|
+
* The transport half of a remote server (`type`, `url`, `headers`), or null
|
|
32806
|
+
* when Crush could not reach it and the server was skipped with a warning.
|
|
32807
|
+
*/
|
|
32808
|
+
function convertRemoteTransport({ name, config, logger }) {
|
|
32809
|
+
const url = resolveRemoteMcpUrl(config);
|
|
32810
|
+
if (!url) {
|
|
32811
|
+
warnAndSkipMcpServer({
|
|
32812
|
+
toolName: "Crush",
|
|
32813
|
+
serverName: name,
|
|
32814
|
+
reason: "a remote transport without a url",
|
|
32815
|
+
logger
|
|
32816
|
+
});
|
|
32817
|
+
return null;
|
|
32818
|
+
}
|
|
32819
|
+
const stated = config.type ?? config.transport;
|
|
32820
|
+
const type = asCrushRemoteType(stated, url);
|
|
32821
|
+
if (type === void 0) {
|
|
32822
|
+
warnAndSkipMcpServer({
|
|
32823
|
+
toolName: "Crush",
|
|
32824
|
+
serverName: name,
|
|
32825
|
+
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)`,
|
|
32826
|
+
logger
|
|
32827
|
+
});
|
|
32828
|
+
return null;
|
|
32829
|
+
}
|
|
32830
|
+
const converted = {
|
|
32831
|
+
type,
|
|
32832
|
+
url
|
|
32833
|
+
};
|
|
32834
|
+
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
32835
|
+
return converted;
|
|
32836
|
+
}
|
|
32837
|
+
/**
|
|
32838
|
+
* The transport half of a stdio server (`type`, `command`, `args`, `env`), or
|
|
32839
|
+
* null when it has no command and was skipped with a warning.
|
|
32840
|
+
*/
|
|
32841
|
+
function convertStdioTransport({ name, config, logger }) {
|
|
32842
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
32843
|
+
if (!command) {
|
|
32844
|
+
warnAndSkipMcpServer({
|
|
32845
|
+
toolName: "Crush",
|
|
32846
|
+
serverName: name,
|
|
32847
|
+
reason: "a stdio transport without a command",
|
|
32848
|
+
logger
|
|
32849
|
+
});
|
|
32850
|
+
return null;
|
|
32851
|
+
}
|
|
32852
|
+
const converted = {
|
|
32853
|
+
type: "stdio",
|
|
32854
|
+
command
|
|
32855
|
+
};
|
|
32856
|
+
if (args.length > 0) converted.args = args;
|
|
32857
|
+
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
32858
|
+
return converted;
|
|
32859
|
+
}
|
|
32860
|
+
/**
|
|
32861
|
+
* Convert canonical rulesync servers to the `mcp.<name>` shape of crush.json.
|
|
32862
|
+
* `type` is required by Crush's schema, so it is always written: stdio servers
|
|
32863
|
+
* carry `type: "stdio"` with `command`/`args`/`env`, remote servers `http` or
|
|
32864
|
+
* `sse` with `url` and optional `headers`. `disabled`, the per-server
|
|
32865
|
+
* `disabledTools`/`enabledTools` filters (`disabled_tools`/`enabled_tools`)
|
|
32866
|
+
* and `timeout` (which Crush reads in seconds) map onto their Crush fields.
|
|
32867
|
+
*
|
|
32868
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
32869
|
+
*/
|
|
32870
|
+
function convertToCrushFormat(mcpServers, logger) {
|
|
32871
|
+
const result = {};
|
|
32872
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
32873
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
32874
|
+
if (!isRecord$1(config)) continue;
|
|
32875
|
+
if (declaresNoTransport(config)) {
|
|
32876
|
+
warnAndSkipMcpServer({
|
|
32877
|
+
toolName: "Crush",
|
|
32878
|
+
serverName: name,
|
|
32879
|
+
reason: "no transport",
|
|
32880
|
+
logger
|
|
32881
|
+
});
|
|
32882
|
+
continue;
|
|
32883
|
+
}
|
|
32884
|
+
const converted = isRemoteMcpServer(config) ? convertRemoteTransport({
|
|
32885
|
+
name,
|
|
32886
|
+
config,
|
|
32887
|
+
logger
|
|
32888
|
+
}) : convertStdioTransport({
|
|
32889
|
+
name,
|
|
32890
|
+
config,
|
|
32891
|
+
logger
|
|
32892
|
+
});
|
|
32893
|
+
if (converted === null) continue;
|
|
32894
|
+
if (config.disabled === true) converted.disabled = true;
|
|
32895
|
+
if (config.disabledTools && config.disabledTools.length > 0) converted.disabled_tools = config.disabledTools;
|
|
32896
|
+
if (config.enabledTools && config.enabledTools.length > 0) converted.enabled_tools = config.enabledTools;
|
|
32897
|
+
if (typeof config.timeout === "number") converted.timeout = Math.ceil(config.timeout);
|
|
32898
|
+
copyCrushOnlyKeys({
|
|
32899
|
+
from: config,
|
|
32900
|
+
to: converted
|
|
32901
|
+
});
|
|
32902
|
+
result[name] = converted;
|
|
32903
|
+
}
|
|
32904
|
+
return result;
|
|
32905
|
+
}
|
|
32906
|
+
/**
|
|
32907
|
+
* Convert the `mcp.<name>` entries of crush.json back to canonical rulesync
|
|
32908
|
+
* servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
|
|
32909
|
+
* server with a `command` is stdio already); `http` and `sse` are kept as the
|
|
32910
|
+
* canonical transport names; `disabled_tools`/`enabled_tools` become the
|
|
32911
|
+
* canonical `disabledTools`/`enabledTools`; the Crush-only OAuth and
|
|
32912
|
+
* `sessionless` fields are lifted into their `crush*` authoring keys (a value
|
|
32913
|
+
* of the wrong type is dropped, as Crush itself would refuse it), and any
|
|
32914
|
+
* other key is kept verbatim so an import preserves what a hand-authored
|
|
32915
|
+
* entry declared.
|
|
32916
|
+
*/
|
|
32917
|
+
function convertFromCrushFormat(crushServers) {
|
|
32918
|
+
const result = {};
|
|
32919
|
+
for (const [name, config] of Object.entries(crushServers)) {
|
|
32920
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
32921
|
+
const converted = {};
|
|
32922
|
+
for (const [key, value] of Object.entries(config)) {
|
|
32923
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32924
|
+
switch (key) {
|
|
32925
|
+
case "type":
|
|
32926
|
+
if (value === "http" || value === "sse") converted.type = value;
|
|
32927
|
+
break;
|
|
32928
|
+
case "disabled_tools":
|
|
32929
|
+
if (isStringArray$2(value)) converted.disabledTools = value;
|
|
32930
|
+
break;
|
|
32931
|
+
case "enabled_tools":
|
|
32932
|
+
if (isStringArray$2(value)) converted.enabledTools = value;
|
|
32933
|
+
break;
|
|
32934
|
+
case "oauth_token": break;
|
|
32935
|
+
default: {
|
|
32936
|
+
const crushOnly = CRUSH_ONLY_KEYS.find((entry) => entry.crush === key);
|
|
32937
|
+
if (crushOnly === void 0) converted[key] = value;
|
|
32938
|
+
else if (crushOnly.accepts(value)) converted[crushOnly.canonical] = value;
|
|
32939
|
+
}
|
|
32940
|
+
}
|
|
32941
|
+
}
|
|
32942
|
+
result[name] = converted;
|
|
32943
|
+
}
|
|
32944
|
+
return result;
|
|
32945
|
+
}
|
|
32946
|
+
/**
|
|
32947
|
+
* Crush MCP servers.
|
|
32948
|
+
*
|
|
32949
|
+
* Crush reads MCP servers from the `mcp` key of its JSON config:
|
|
32950
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
32951
|
+
* objects recursively) at project scope and `~/.config/crush/crush.json` at
|
|
32952
|
+
* user scope. The
|
|
32953
|
+
* `crushrc` Bash config that Crush now recommends compiles its `mcp add`
|
|
32954
|
+
* builtin into the same `mcp.<name>` entries and overrides the JSON key by
|
|
32955
|
+
* key, so a `crushrc` next to the generated file takes precedence. Every
|
|
32956
|
+
* other top-level key of the file is preserved; the file is never deleted.
|
|
32957
|
+
*
|
|
32958
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
32959
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
32960
|
+
*/
|
|
32961
|
+
var CrushMcp = class CrushMcp extends ToolMcp {
|
|
32962
|
+
json;
|
|
32963
|
+
constructor(params) {
|
|
32964
|
+
super(params);
|
|
32965
|
+
this.json = parseCrushConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
32966
|
+
}
|
|
32967
|
+
getJson() {
|
|
32968
|
+
return this.json;
|
|
32969
|
+
}
|
|
32970
|
+
isDeletable() {
|
|
32971
|
+
return false;
|
|
32972
|
+
}
|
|
32973
|
+
static getSettablePaths({ global = false } = {}) {
|
|
32974
|
+
return getCrushConfigSettablePaths({ global });
|
|
32975
|
+
}
|
|
32976
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32977
|
+
const location = await resolveCrushConfigFile({
|
|
32978
|
+
outputRoot,
|
|
32979
|
+
global
|
|
32980
|
+
});
|
|
32981
|
+
return new CrushMcp({
|
|
32982
|
+
outputRoot,
|
|
32983
|
+
relativeDirPath: location.relativeDirPath,
|
|
32984
|
+
relativeFilePath: location.relativeFilePath,
|
|
32985
|
+
fileContent: crushConfigImportContent(location),
|
|
32986
|
+
validate,
|
|
32987
|
+
global
|
|
32988
|
+
});
|
|
32989
|
+
}
|
|
32990
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
32991
|
+
const location = await resolveCrushConfigFile({
|
|
32992
|
+
outputRoot,
|
|
32993
|
+
global
|
|
32994
|
+
});
|
|
32995
|
+
const existingContent = location.fileContent ?? "";
|
|
32996
|
+
warnCrushTwinLeftovers({
|
|
32997
|
+
location,
|
|
32998
|
+
ownedPaths: [["mcp"]],
|
|
32999
|
+
logger
|
|
33000
|
+
});
|
|
33001
|
+
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
|
|
33002
|
+
...serverConfig,
|
|
33003
|
+
...readCrushOnlyKeys(rulesyncMcp.getRawMcpServer(serverName))
|
|
33004
|
+
}])), logger);
|
|
33005
|
+
return new CrushMcp({
|
|
33006
|
+
outputRoot,
|
|
33007
|
+
relativeDirPath: location.relativeDirPath,
|
|
33008
|
+
relativeFilePath: location.relativeFilePath,
|
|
33009
|
+
fileContent: applySharedConfigPatch({
|
|
33010
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
33011
|
+
feature: "mcp",
|
|
33012
|
+
existingContent,
|
|
33013
|
+
patch: { ["mcp"]: converted },
|
|
33014
|
+
filePath: location.filePath,
|
|
33015
|
+
logger
|
|
33016
|
+
}),
|
|
33017
|
+
validate,
|
|
33018
|
+
global
|
|
33019
|
+
});
|
|
33020
|
+
}
|
|
33021
|
+
toRulesyncMcp() {
|
|
33022
|
+
const converted = convertFromCrushFormat(isRecord$1(this.json["mcp"]) ? this.json["mcp"] : {});
|
|
33023
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
33024
|
+
}
|
|
33025
|
+
validate() {
|
|
33026
|
+
return {
|
|
33027
|
+
success: true,
|
|
33028
|
+
error: null
|
|
33029
|
+
};
|
|
33030
|
+
}
|
|
33031
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
33032
|
+
return new CrushMcp({
|
|
33033
|
+
outputRoot,
|
|
33034
|
+
relativeDirPath,
|
|
33035
|
+
relativeFilePath,
|
|
33036
|
+
fileContent: JSON.stringify({ ["mcp"]: {} }, null, 2),
|
|
33037
|
+
validate: false,
|
|
33038
|
+
global
|
|
33039
|
+
});
|
|
33040
|
+
}
|
|
33041
|
+
};
|
|
33042
|
+
//#endregion
|
|
32439
33043
|
//#region src/features/mcp/mcp-env-var-format.ts
|
|
32440
33044
|
/**
|
|
32441
33045
|
* Canonical rulesync env var reference pattern: `${VAR}` (but not `${env:VAR}`,
|
|
@@ -36480,7 +37084,7 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36480
37084
|
}
|
|
36481
37085
|
for (const [key, value] of Object.entries(rest)) {
|
|
36482
37086
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36483
|
-
converted[key] = (
|
|
37087
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36484
37088
|
}
|
|
36485
37089
|
if (enabledTools !== void 0) converted.includeTools = enabledTools;
|
|
36486
37090
|
if (disabledTools !== void 0) converted.excludeTools = disabledTools;
|
|
@@ -36491,7 +37095,9 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36491
37095
|
/**
|
|
36492
37096
|
* Convert Tabnine's server map back to the canonical shape: `includeTools` /
|
|
36493
37097
|
* `excludeTools` become `enabledTools` / `disabledTools`; `type` (`stdio`,
|
|
36494
|
-
* `sse`, `http`) and every other documented field are already canonical
|
|
37098
|
+
* `sse`, `http`) and every other documented field are already canonical and
|
|
37099
|
+
* pass through with their prototype-pollution keys dropped at every nesting
|
|
37100
|
+
* level, mirroring the generate side.
|
|
36495
37101
|
*/
|
|
36496
37102
|
function convertFromTabnineFormat(mcpServers) {
|
|
36497
37103
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -36503,7 +37109,7 @@ function convertFromTabnineFormat(mcpServers) {
|
|
|
36503
37109
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36504
37110
|
if (key === "includeTools") converted.enabledTools = value;
|
|
36505
37111
|
else if (key === "excludeTools") converted.disabledTools = value;
|
|
36506
|
-
else converted[key] = value;
|
|
37112
|
+
else converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36507
37113
|
}
|
|
36508
37114
|
result[serverName] = converted;
|
|
36509
37115
|
}
|
|
@@ -37651,6 +38257,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37651
38257
|
supportsDisabledTools: false
|
|
37652
38258
|
}
|
|
37653
38259
|
}],
|
|
38260
|
+
["crush", {
|
|
38261
|
+
class: CrushMcp,
|
|
38262
|
+
meta: {
|
|
38263
|
+
supportsProject: true,
|
|
38264
|
+
supportsGlobal: true,
|
|
38265
|
+
supportsEnabledTools: true,
|
|
38266
|
+
supportsDisabledTools: true
|
|
38267
|
+
}
|
|
38268
|
+
}],
|
|
37654
38269
|
["cursor", {
|
|
37655
38270
|
class: CursorMcp,
|
|
37656
38271
|
meta: {
|
|
@@ -39082,7 +39697,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
39082
39697
|
function toAntigravityCliToolName(canonical) {
|
|
39083
39698
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
39084
39699
|
}
|
|
39085
|
-
function toCanonicalToolName$
|
|
39700
|
+
function toCanonicalToolName$8(cliName) {
|
|
39086
39701
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
39087
39702
|
}
|
|
39088
39703
|
/**
|
|
@@ -39298,7 +39913,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
39298
39913
|
const processEntries = (entries, action) => {
|
|
39299
39914
|
for (const entry of entries) {
|
|
39300
39915
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
39301
|
-
const canonical = toCanonicalToolName$
|
|
39916
|
+
const canonical = toCanonicalToolName$8(toolName);
|
|
39302
39917
|
if (!permission[canonical]) permission[canonical] = {};
|
|
39303
39918
|
permission[canonical][pattern] = action;
|
|
39304
39919
|
}
|
|
@@ -39555,11 +40170,34 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
|
|
|
39555
40170
|
websearch: "web-search"
|
|
39556
40171
|
};
|
|
39557
40172
|
const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
40173
|
+
const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
|
|
40174
|
+
terminal: "launch-process",
|
|
40175
|
+
read: "view",
|
|
40176
|
+
edit: "str-replace-editor",
|
|
40177
|
+
write: "save-file"
|
|
40178
|
+
};
|
|
39558
40179
|
function toAugmentToolName(canonical) {
|
|
39559
|
-
return
|
|
40180
|
+
return lookupOwn({
|
|
40181
|
+
record: CANONICAL_TO_AUGMENT_TOOL_NAMES,
|
|
40182
|
+
key: canonical
|
|
40183
|
+
}) ?? canonical;
|
|
39560
40184
|
}
|
|
39561
|
-
|
|
39562
|
-
|
|
40185
|
+
/**
|
|
40186
|
+
* Resolve an existing entry's `toolName` to the legacy spelling rulesync emits, so that the
|
|
40187
|
+
* current alias and the legacy name are treated as the same managed tool.
|
|
40188
|
+
*/
|
|
40189
|
+
function toLegacyAugmentToolName(augmentName) {
|
|
40190
|
+
return lookupOwn({
|
|
40191
|
+
record: CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES,
|
|
40192
|
+
key: augmentName
|
|
40193
|
+
}) ?? augmentName;
|
|
40194
|
+
}
|
|
40195
|
+
function toCanonicalToolName$7(augmentName) {
|
|
40196
|
+
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
40197
|
+
return lookupOwn({
|
|
40198
|
+
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
40199
|
+
key: legacyName
|
|
40200
|
+
}) ?? legacyName;
|
|
39563
40201
|
}
|
|
39564
40202
|
function actionToAugmentType(action) {
|
|
39565
40203
|
switch (action) {
|
|
@@ -39775,10 +40413,11 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
39775
40413
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
39776
40414
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
39777
40415
|
if (entry.toolName === "*") return false;
|
|
39778
|
-
|
|
40416
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40417
|
+
if (!MANAGED_AUGMENT_TOOL_NAMES.has(legacyToolName)) return true;
|
|
39779
40418
|
if (entry.permission.type === "deny") {
|
|
39780
|
-
const
|
|
39781
|
-
return !generatedKeys.has(
|
|
40419
|
+
const suffix = `|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
40420
|
+
return !generatedKeys.has(`${entry.toolName}${suffix}`) && !generatedKeys.has(`${legacyToolName}${suffix}`);
|
|
39782
40421
|
}
|
|
39783
40422
|
return false;
|
|
39784
40423
|
});
|
|
@@ -40005,14 +40644,15 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40005
40644
|
if (isSpecialEntry(entry)) continue;
|
|
40006
40645
|
const type = entry.permission.type;
|
|
40007
40646
|
if (!isBasicAugmentType(type)) continue;
|
|
40008
|
-
const
|
|
40647
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40648
|
+
const canonical = toCanonicalToolName$7(entry.toolName);
|
|
40009
40649
|
if (forbiddenMapKeys.has(canonical)) {
|
|
40010
40650
|
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.`);
|
|
40011
40651
|
continue;
|
|
40012
40652
|
}
|
|
40013
40653
|
const action = augmentTypeToAction(type);
|
|
40014
40654
|
let pattern;
|
|
40015
|
-
if (
|
|
40655
|
+
if (legacyToolName === "launch-process" && entry.shellInputRegex) {
|
|
40016
40656
|
const regex = entry.shellInputRegex;
|
|
40017
40657
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
40018
40658
|
else if (action === "deny") {
|
|
@@ -40027,9 +40667,19 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40027
40667
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because its pattern resolves to the reserved object key '${pattern}'.`);
|
|
40028
40668
|
continue;
|
|
40029
40669
|
}
|
|
40030
|
-
|
|
40031
|
-
|
|
40032
|
-
|
|
40670
|
+
let bucket = lookupOwn({
|
|
40671
|
+
record: permission,
|
|
40672
|
+
key: canonical
|
|
40673
|
+
});
|
|
40674
|
+
if (bucket === void 0) {
|
|
40675
|
+
bucket = {};
|
|
40676
|
+
permission[canonical] = bucket;
|
|
40677
|
+
}
|
|
40678
|
+
const existing = lookupOwn({
|
|
40679
|
+
record: bucket,
|
|
40680
|
+
key: pattern
|
|
40681
|
+
});
|
|
40682
|
+
if (existing === void 0 || actionPriority[action] > actionPriority[existing]) bucket[pattern] = action;
|
|
40033
40683
|
}
|
|
40034
40684
|
return { permission };
|
|
40035
40685
|
}
|
|
@@ -40240,7 +40890,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
40240
40890
|
function toClaudeToolName(canonical) {
|
|
40241
40891
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
40242
40892
|
}
|
|
40243
|
-
function toCanonicalToolName$
|
|
40893
|
+
function toCanonicalToolName$6(claudeName) {
|
|
40244
40894
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
40245
40895
|
}
|
|
40246
40896
|
/**
|
|
@@ -41135,7 +41785,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
41135
41785
|
const processEntries = (entries, action) => {
|
|
41136
41786
|
for (const entry of entries) {
|
|
41137
41787
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
41138
|
-
const canonical = toCanonicalToolName$
|
|
41788
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
41139
41789
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41140
41790
|
permission[canonical][pattern] = action;
|
|
41141
41791
|
}
|
|
@@ -41960,7 +42610,16 @@ function mapBashActionToDecision(action) {
|
|
|
41960
42610
|
//#endregion
|
|
41961
42611
|
//#region src/features/permissions/continue-permissions.ts
|
|
41962
42612
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
41963
|
-
const CATCH_ALL_PATTERN$
|
|
42613
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
42614
|
+
/**
|
|
42615
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
42616
|
+
* would match `Tool(**)` against every argument just as the bare tool name
|
|
42617
|
+
* does, so it is written — and, under the all-tools category, honored — as
|
|
42618
|
+
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
42619
|
+
*/
|
|
42620
|
+
function normalizeCatchAllPattern$1(pattern) {
|
|
42621
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
42622
|
+
}
|
|
41964
42623
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
41965
42624
|
bash: "Bash",
|
|
41966
42625
|
read: "Read",
|
|
@@ -41988,7 +42647,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
41988
42647
|
function toContinueToolName(canonical) {
|
|
41989
42648
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
41990
42649
|
}
|
|
41991
|
-
function toCanonicalToolName$
|
|
42650
|
+
function toCanonicalToolName$5(continueName) {
|
|
41992
42651
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
41993
42652
|
}
|
|
41994
42653
|
/**
|
|
@@ -41996,7 +42655,7 @@ function toCanonicalToolName$4(continueName) {
|
|
|
41996
42655
|
* `Tool(pattern)`.
|
|
41997
42656
|
*/
|
|
41998
42657
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
41999
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
42658
|
+
return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
|
|
42000
42659
|
}
|
|
42001
42660
|
/**
|
|
42002
42661
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -42010,7 +42669,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
42010
42669
|
if (toolName === "") return;
|
|
42011
42670
|
return {
|
|
42012
42671
|
toolName,
|
|
42013
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
42672
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
|
|
42014
42673
|
};
|
|
42015
42674
|
}
|
|
42016
42675
|
/**
|
|
@@ -42177,13 +42836,14 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
42177
42836
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42178
42837
|
if (isPrototypePollutionKey(category)) continue;
|
|
42179
42838
|
const toolName = category === "*" ? "*" : toContinueToolName(category);
|
|
42180
|
-
for (const [
|
|
42181
|
-
if (isPrototypePollutionKey(
|
|
42839
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
42840
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
42841
|
+
const pattern = normalizeCatchAllPattern$1(rawPattern);
|
|
42182
42842
|
if (pattern.includes("(") || pattern.includes(")")) {
|
|
42183
42843
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42184
42844
|
continue;
|
|
42185
42845
|
}
|
|
42186
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
42846
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
|
|
42187
42847
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42188
42848
|
continue;
|
|
42189
42849
|
}
|
|
@@ -42208,7 +42868,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
42208
42868
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
42209
42869
|
if (parsedEntry === void 0) continue;
|
|
42210
42870
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
42211
|
-
const category = toCanonicalToolName$
|
|
42871
|
+
const category = toCanonicalToolName$5(parsedEntry.toolName);
|
|
42212
42872
|
permission[category] ??= {};
|
|
42213
42873
|
permission[category][parsedEntry.pattern] ??= action;
|
|
42214
42874
|
}
|
|
@@ -42531,6 +43191,295 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
42531
43191
|
}
|
|
42532
43192
|
};
|
|
42533
43193
|
//#endregion
|
|
43194
|
+
//#region src/features/permissions/crush-permissions.ts
|
|
43195
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
43196
|
+
/**
|
|
43197
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
43198
|
+
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
43199
|
+
* still be written.
|
|
43200
|
+
*/
|
|
43201
|
+
function normalizeCatchAllPattern(pattern) {
|
|
43202
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
|
|
43203
|
+
}
|
|
43204
|
+
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
43205
|
+
bash: "bash",
|
|
43206
|
+
read: "view",
|
|
43207
|
+
edit: "edit",
|
|
43208
|
+
write: "write",
|
|
43209
|
+
grep: "grep",
|
|
43210
|
+
glob: "glob",
|
|
43211
|
+
webfetch: "fetch"
|
|
43212
|
+
};
|
|
43213
|
+
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
43214
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
43215
|
+
const MCP_CRUSH_PREFIX = "mcp_";
|
|
43216
|
+
function toCrushToolName(canonical) {
|
|
43217
|
+
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
43218
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
43219
|
+
return canonical;
|
|
43220
|
+
}
|
|
43221
|
+
function toCanonicalToolName$4(crushName) {
|
|
43222
|
+
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
43223
|
+
}
|
|
43224
|
+
function isMcpToolName(crushName) {
|
|
43225
|
+
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
43226
|
+
}
|
|
43227
|
+
function uniq$9(values) {
|
|
43228
|
+
return [...new Set(values)];
|
|
43229
|
+
}
|
|
43230
|
+
/**
|
|
43231
|
+
* Permissions adapter for Crush.
|
|
43232
|
+
*
|
|
43233
|
+
* Crush has no per-argument permission rules. Its JSON config carries two
|
|
43234
|
+
* tool-level lists instead:
|
|
43235
|
+
* - `permissions.allowed_tools`: tools (or `tool:action` pairs) that run
|
|
43236
|
+
* without a permission prompt; everything else prompts.
|
|
43237
|
+
* - `options.disabled_tools`: built-in tools removed from the agent
|
|
43238
|
+
* entirely.
|
|
43239
|
+
*
|
|
43240
|
+
* Mapping (rulesync canonical -> Crush), catch-all (`*`) rules only:
|
|
43241
|
+
* - `allow` -> an `allowed_tools` entry.
|
|
43242
|
+
* - `deny` -> a `disabled_tools` entry. Crush only filters its built-ins
|
|
43243
|
+
* through that list, so a deny for an MCP tool is reported and skipped
|
|
43244
|
+
* (disable the tool in the MCP server's `disabled_tools` instead).
|
|
43245
|
+
* - `ask` -> nothing (prompting is Crush's default).
|
|
43246
|
+
* - Tool name: `bash` -> `bash`, `read` -> `view`, `edit` -> `edit`,
|
|
43247
|
+
* `write` -> `write`, `grep` -> `grep`, `glob` -> `glob`,
|
|
43248
|
+
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
43249
|
+
* any other category passes through verbatim.
|
|
43250
|
+
*
|
|
43251
|
+
* A pattern-specific rule cannot be expressed and is reported and skipped.
|
|
43252
|
+
* Because Crush cannot narrow an allowed tool, a category's catch-all `allow`
|
|
43253
|
+
* is not written while the same category carries a pattern-specific `deny` or
|
|
43254
|
+
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
43255
|
+
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
43256
|
+
* `honorAllToolsOnBash` and otherwise skipped, since Crush has no allow-all
|
|
43257
|
+
* or disable-all list.
|
|
43258
|
+
*
|
|
43259
|
+
* `crush.json` is Crush's main config: entries naming a tool the canonical
|
|
43260
|
+
* config does not manage, and every `tool:action` entry (a form rulesync
|
|
43261
|
+
* never derives), are preserved verbatim; the managed tools' bare entries
|
|
43262
|
+
* are rebuilt, every other key of `permissions` / `options` and of the file is
|
|
43263
|
+
* kept, and the file is never deleted. Project scope writes `crush.json`, or
|
|
43264
|
+
* an existing `.crush.json`; Crush merges the pair (lists concatenated), so an
|
|
43265
|
+
* entry left in the other file stays in effect and is reported.
|
|
43266
|
+
*
|
|
43267
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
43268
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
43269
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/permission/permission.go
|
|
43270
|
+
*/
|
|
43271
|
+
var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
43272
|
+
json;
|
|
43273
|
+
constructor(params) {
|
|
43274
|
+
super({
|
|
43275
|
+
...params,
|
|
43276
|
+
fileContent: params.fileContent ?? ""
|
|
43277
|
+
});
|
|
43278
|
+
this.json = parseCrushConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
43279
|
+
}
|
|
43280
|
+
getJson() {
|
|
43281
|
+
return this.json;
|
|
43282
|
+
}
|
|
43283
|
+
isDeletable() {
|
|
43284
|
+
return false;
|
|
43285
|
+
}
|
|
43286
|
+
/**
|
|
43287
|
+
* `crush.json` is Crush's file: rulesync merges into it when it exists but
|
|
43288
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
43289
|
+
* absent `allowed_tools` both mean "prompt for everything".
|
|
43290
|
+
*/
|
|
43291
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
43292
|
+
return true;
|
|
43293
|
+
}
|
|
43294
|
+
static getSettablePaths({ global = false } = {}) {
|
|
43295
|
+
return getCrushConfigSettablePaths({ global });
|
|
43296
|
+
}
|
|
43297
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
43298
|
+
const location = await resolveCrushConfigFile({
|
|
43299
|
+
outputRoot,
|
|
43300
|
+
global
|
|
43301
|
+
});
|
|
43302
|
+
return new CrushPermissions({
|
|
43303
|
+
outputRoot,
|
|
43304
|
+
relativeDirPath: location.relativeDirPath,
|
|
43305
|
+
relativeFilePath: location.relativeFilePath,
|
|
43306
|
+
fileContent: crushConfigImportContent(location),
|
|
43307
|
+
validate,
|
|
43308
|
+
global
|
|
43309
|
+
});
|
|
43310
|
+
}
|
|
43311
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
43312
|
+
const location = await resolveCrushConfigFile({
|
|
43313
|
+
outputRoot,
|
|
43314
|
+
global
|
|
43315
|
+
});
|
|
43316
|
+
const existingContent = location.fileContent ?? "";
|
|
43317
|
+
warnCrushTwinLeftovers({
|
|
43318
|
+
location,
|
|
43319
|
+
ownedPaths: [[CRUSH_PERMISSIONS_KEY, CRUSH_ALLOWED_TOOLS_KEY], [CRUSH_OPTIONS_KEY, CRUSH_DISABLED_TOOLS_KEY]],
|
|
43320
|
+
logger
|
|
43321
|
+
});
|
|
43322
|
+
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
43323
|
+
const existingPermissions = isRecord$1(existing["permissions"]) ? existing[CRUSH_PERMISSIONS_KEY] : void 0;
|
|
43324
|
+
const existingOptions = isRecord$1(existing["options"]) ? existing[CRUSH_OPTIONS_KEY] : void 0;
|
|
43325
|
+
const config = rulesyncPermissions.getJson();
|
|
43326
|
+
const generated = convertRulesyncToCrushLists({
|
|
43327
|
+
config,
|
|
43328
|
+
logger
|
|
43329
|
+
});
|
|
43330
|
+
const managedToolNames = managedCrushToolNames(config);
|
|
43331
|
+
const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
43332
|
+
const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
43333
|
+
const allowedList = uniq$9([...preservedAllowed, ...generated.allowed]);
|
|
43334
|
+
const disabledList = uniq$9([...preservedDisabled, ...generated.disabled]);
|
|
43335
|
+
const patch = {};
|
|
43336
|
+
if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
|
|
43337
|
+
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
43338
|
+
return new CrushPermissions({
|
|
43339
|
+
outputRoot,
|
|
43340
|
+
relativeDirPath: location.relativeDirPath,
|
|
43341
|
+
relativeFilePath: location.relativeFilePath,
|
|
43342
|
+
fileContent: applySharedConfigPatch({
|
|
43343
|
+
fileKey: sharedConfigFileKey(CrushPermissions.getSettablePaths({ global })),
|
|
43344
|
+
feature: "permissions",
|
|
43345
|
+
existingContent,
|
|
43346
|
+
patch,
|
|
43347
|
+
filePath: location.filePath,
|
|
43348
|
+
logger
|
|
43349
|
+
}),
|
|
43350
|
+
validate: true,
|
|
43351
|
+
global
|
|
43352
|
+
});
|
|
43353
|
+
}
|
|
43354
|
+
toRulesyncPermissions() {
|
|
43355
|
+
const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
43356
|
+
const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
|
|
43357
|
+
const rulesyncConfig = convertCrushListsToRulesync({
|
|
43358
|
+
allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
|
|
43359
|
+
disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
|
|
43360
|
+
});
|
|
43361
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
43362
|
+
}
|
|
43363
|
+
validate() {
|
|
43364
|
+
return {
|
|
43365
|
+
success: true,
|
|
43366
|
+
error: null
|
|
43367
|
+
};
|
|
43368
|
+
}
|
|
43369
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
43370
|
+
return new CrushPermissions({
|
|
43371
|
+
outputRoot,
|
|
43372
|
+
relativeDirPath,
|
|
43373
|
+
relativeFilePath,
|
|
43374
|
+
fileContent: "{}",
|
|
43375
|
+
validate: false,
|
|
43376
|
+
global
|
|
43377
|
+
});
|
|
43378
|
+
}
|
|
43379
|
+
};
|
|
43380
|
+
/**
|
|
43381
|
+
* The Crush tool names the canonical config manages, i.e. the names its
|
|
43382
|
+
* categories map to. Entries naming any other tool are the user's and survive
|
|
43383
|
+
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
43384
|
+
*/
|
|
43385
|
+
function managedCrushToolNames(config) {
|
|
43386
|
+
return new Set(Object.keys(config.permission).filter((category) => category !== "*" && !isPrototypePollutionKey(category)).map((category) => toCrushToolName(category)));
|
|
43387
|
+
}
|
|
43388
|
+
/**
|
|
43389
|
+
* Reduce one category's rules to the single tool-wide action Crush can carry,
|
|
43390
|
+
* or undefined when nothing should be written for the tool: no catch-all rule,
|
|
43391
|
+
* a catch-all allow next to a narrower deny/ask (Crush cannot narrow an
|
|
43392
|
+
* allowed tool, so it keeps prompting instead), or a deny on an MCP tool
|
|
43393
|
+
* (`options.disabled_tools` only covers built-ins).
|
|
43394
|
+
*/
|
|
43395
|
+
function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
43396
|
+
let catchAllAction;
|
|
43397
|
+
let restricted = false;
|
|
43398
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
43399
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
43400
|
+
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
43401
|
+
if (pattern === CATCH_ALL_PATTERN$4) {
|
|
43402
|
+
catchAllAction = action;
|
|
43403
|
+
continue;
|
|
43404
|
+
}
|
|
43405
|
+
logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
43406
|
+
if (action !== "allow") restricted = true;
|
|
43407
|
+
}
|
|
43408
|
+
if (catchAllAction === "allow" && restricted) {
|
|
43409
|
+
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}".`);
|
|
43410
|
+
return;
|
|
43411
|
+
}
|
|
43412
|
+
if (catchAllAction === "deny" && isMcpToolName(toolName)) {
|
|
43413
|
+
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.`);
|
|
43414
|
+
return;
|
|
43415
|
+
}
|
|
43416
|
+
return catchAllAction;
|
|
43417
|
+
}
|
|
43418
|
+
/**
|
|
43419
|
+
* Convert a rulesync permissions config into Crush's two tool lists.
|
|
43420
|
+
*/
|
|
43421
|
+
function convertRulesyncToCrushLists({ config, logger }) {
|
|
43422
|
+
const lists = {
|
|
43423
|
+
allowed: [],
|
|
43424
|
+
disabled: []
|
|
43425
|
+
};
|
|
43426
|
+
const actionByTool = /* @__PURE__ */ new Map();
|
|
43427
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
43428
|
+
if (isPrototypePollutionKey(category)) continue;
|
|
43429
|
+
if (category === "*") {
|
|
43430
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
43431
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
43432
|
+
logger?.warn(`Crush has no allow-all or disable-all tool list, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
43433
|
+
}
|
|
43434
|
+
continue;
|
|
43435
|
+
}
|
|
43436
|
+
const toolName = toCrushToolName(category);
|
|
43437
|
+
const catchAllAction = resolveCategoryAction({
|
|
43438
|
+
category,
|
|
43439
|
+
toolName,
|
|
43440
|
+
rules,
|
|
43441
|
+
logger
|
|
43442
|
+
});
|
|
43443
|
+
if (catchAllAction === void 0) continue;
|
|
43444
|
+
const previous = actionByTool.get(toolName);
|
|
43445
|
+
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.`);
|
|
43446
|
+
actionByTool.set(toolName, catchAllAction);
|
|
43447
|
+
if (catchAllAction === "allow") lists.allowed.push(toolName);
|
|
43448
|
+
else if (catchAllAction === "deny") lists.disabled.push(toolName);
|
|
43449
|
+
}
|
|
43450
|
+
return {
|
|
43451
|
+
allowed: uniq$9(lists.allowed),
|
|
43452
|
+
disabled: uniq$9(lists.disabled)
|
|
43453
|
+
};
|
|
43454
|
+
}
|
|
43455
|
+
/**
|
|
43456
|
+
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
43457
|
+
* entry scopes an allow to one action of a tool, which the canonical model
|
|
43458
|
+
* cannot express, so it is left out rather than widened to the whole tool.
|
|
43459
|
+
*/
|
|
43460
|
+
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
43461
|
+
const permission = {};
|
|
43462
|
+
const bucketFor = (category) => {
|
|
43463
|
+
const own = lookupOwn({
|
|
43464
|
+
record: permission,
|
|
43465
|
+
key: category
|
|
43466
|
+
});
|
|
43467
|
+
if (own !== void 0) return own;
|
|
43468
|
+
const created = {};
|
|
43469
|
+
permission[category] = created;
|
|
43470
|
+
return created;
|
|
43471
|
+
};
|
|
43472
|
+
for (const entry of disabled) {
|
|
43473
|
+
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
43474
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
|
|
43475
|
+
}
|
|
43476
|
+
for (const entry of allowed) {
|
|
43477
|
+
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
43478
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
|
|
43479
|
+
}
|
|
43480
|
+
return { permission };
|
|
43481
|
+
}
|
|
43482
|
+
//#endregion
|
|
42534
43483
|
//#region src/features/permissions/cursor-permissions.ts
|
|
42535
43484
|
/**
|
|
42536
43485
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -43685,6 +44634,40 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43685
44634
|
return Object.keys(bash).length > 0 ? { permission: { bash } } : { permission: {} };
|
|
43686
44635
|
}
|
|
43687
44636
|
//#endregion
|
|
44637
|
+
//#region src/features/permissions/single-action-collapse.ts
|
|
44638
|
+
/**
|
|
44639
|
+
* Strictness order shared by the tools whose permission model evaluates
|
|
44640
|
+
* `deny > ask > allow`. Used wherever several canonical rules have to fold
|
|
44641
|
+
* into one entry, so the strictest action always wins.
|
|
44642
|
+
*/
|
|
44643
|
+
const PERMISSION_ACTION_PRIORITY = {
|
|
44644
|
+
allow: 0,
|
|
44645
|
+
ask: 1,
|
|
44646
|
+
deny: 2
|
|
44647
|
+
};
|
|
44648
|
+
/**
|
|
44649
|
+
* Collapse a pattern map to the single action a tool can hold for a scope
|
|
44650
|
+
* that has no pattern matcher, using deny > ask > allow precedence.
|
|
44651
|
+
*
|
|
44652
|
+
* A map without a catch-all grants nothing to unmatched inputs, so an
|
|
44653
|
+
* implicit `ask` joins the candidates and a narrow allowlist can never widen
|
|
44654
|
+
* into a blanket allow. Returns `undefined` for an empty map; each caller
|
|
44655
|
+
* decides what an empty map means for its tool (e.g. emit nothing, or fall
|
|
44656
|
+
* back to `deny` when the tool's own default is allow).
|
|
44657
|
+
*/
|
|
44658
|
+
function collapseRulesToSingleAction({ rules }) {
|
|
44659
|
+
const actions = Object.values(rules);
|
|
44660
|
+
if (actions.length === 0) return;
|
|
44661
|
+
return (Object.hasOwn(rules, "*") ? actions : [...actions, "ask"]).reduce((current, candidate) => PERMISSION_ACTION_PRIORITY[candidate] > PERMISSION_ACTION_PRIORITY[current] ? candidate : current);
|
|
44662
|
+
}
|
|
44663
|
+
/**
|
|
44664
|
+
* Whether a pattern map carries anything beyond the `*` catch-all, i.e.
|
|
44665
|
+
* whether collapsing it to a single action loses information worth a warning.
|
|
44666
|
+
*/
|
|
44667
|
+
function hasPatternSpecificRules(rules) {
|
|
44668
|
+
return Object.keys(rules).some((pattern) => pattern !== "*");
|
|
44669
|
+
}
|
|
44670
|
+
//#endregion
|
|
43688
44671
|
//#region src/features/permissions/devin-permissions.ts
|
|
43689
44672
|
/**
|
|
43690
44673
|
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
@@ -43694,17 +44677,21 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43694
44677
|
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
43695
44678
|
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
43696
44679
|
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
43697
|
-
* `edit` rules round-trip as `write` (a lossy but documented collapse).
|
|
44680
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). The
|
|
44681
|
+
* canonical `websearch` category maps onto the bare `web_search` tool name,
|
|
44682
|
+
* accepted in the permission lists since CLI v3000.10.21 (2026-09-10). Unknown
|
|
43698
44683
|
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
43699
44684
|
*
|
|
43700
44685
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
44686
|
+
* @see https://docs.devin.ai/cli/changelog/stable — v3000.10.21, `web_search`
|
|
43701
44687
|
*/
|
|
43702
44688
|
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
43703
44689
|
read: "Read",
|
|
43704
44690
|
write: "Write",
|
|
43705
44691
|
edit: "Write",
|
|
43706
44692
|
bash: "Exec",
|
|
43707
|
-
webfetch: "Fetch"
|
|
44693
|
+
webfetch: "Fetch",
|
|
44694
|
+
websearch: "web_search"
|
|
43708
44695
|
};
|
|
43709
44696
|
/**
|
|
43710
44697
|
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
@@ -43713,8 +44700,15 @@ const DEVIN_SCOPE_TO_CANONICAL = {
|
|
|
43713
44700
|
Read: "read",
|
|
43714
44701
|
Write: "write",
|
|
43715
44702
|
Exec: "bash",
|
|
43716
|
-
Fetch: "webfetch"
|
|
44703
|
+
Fetch: "webfetch",
|
|
44704
|
+
web_search: "websearch"
|
|
43717
44705
|
};
|
|
44706
|
+
/**
|
|
44707
|
+
* Devin scopes that exist only as a bare tool name: there is no
|
|
44708
|
+
* `web_search(pattern)` matcher, so a pattern-specific rule under the canonical
|
|
44709
|
+
* category cannot be expressed and is collapsed to one action instead.
|
|
44710
|
+
*/
|
|
44711
|
+
const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
|
|
43718
44712
|
function toDevinScope(canonical) {
|
|
43719
44713
|
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
43720
44714
|
}
|
|
@@ -43899,7 +44893,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43899
44893
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
43900
44894
|
}
|
|
43901
44895
|
const config = rulesyncPermissions.getJson();
|
|
43902
|
-
const { allow, ask, deny } = convertRulesyncToDevinPermissions(
|
|
44896
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions({
|
|
44897
|
+
config,
|
|
44898
|
+
logger
|
|
44899
|
+
});
|
|
43903
44900
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
43904
44901
|
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
43905
44902
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
@@ -44003,26 +45000,50 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
44003
45000
|
}
|
|
44004
45001
|
};
|
|
44005
45002
|
/**
|
|
45003
|
+
* Collapse the pattern rules of a bare-only scope to the single action Devin
|
|
45004
|
+
* can hold for it, using deny > ask > allow precedence. A map without a
|
|
45005
|
+
* catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
|
|
45006
|
+
* candidates and a narrow allowlist can never widen into a blanket allow.
|
|
45007
|
+
* Returns `undefined` for an empty map: like any other empty category it
|
|
45008
|
+
* emits nothing, which leaves Devin's own auto-approve default in place.
|
|
45009
|
+
*/
|
|
45010
|
+
function collapseBareOnlyScopeRules({ category, scope, rules, logger }) {
|
|
45011
|
+
const action = collapseRulesToSingleAction({ rules });
|
|
45012
|
+
if (action === void 0) return;
|
|
45013
|
+
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.`);
|
|
45014
|
+
return action;
|
|
45015
|
+
}
|
|
45016
|
+
/**
|
|
44006
45017
|
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
44007
45018
|
*/
|
|
44008
|
-
function convertRulesyncToDevinPermissions(config) {
|
|
45019
|
+
function convertRulesyncToDevinPermissions({ config, logger }) {
|
|
44009
45020
|
const allow = [];
|
|
44010
45021
|
const ask = [];
|
|
44011
45022
|
const deny = [];
|
|
45023
|
+
const push = (entry, action) => {
|
|
45024
|
+
switch (action) {
|
|
45025
|
+
case "allow":
|
|
45026
|
+
allow.push(entry);
|
|
45027
|
+
break;
|
|
45028
|
+
case "ask":
|
|
45029
|
+
ask.push(entry);
|
|
45030
|
+
break;
|
|
45031
|
+
case "deny": deny.push(entry);
|
|
45032
|
+
}
|
|
45033
|
+
};
|
|
44012
45034
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
44013
45035
|
const scope = toDevinScope(category);
|
|
44014
|
-
|
|
44015
|
-
const
|
|
44016
|
-
|
|
44017
|
-
|
|
44018
|
-
|
|
44019
|
-
|
|
44020
|
-
|
|
44021
|
-
|
|
44022
|
-
|
|
44023
|
-
case "deny": deny.push(entry);
|
|
44024
|
-
}
|
|
45036
|
+
if (DEVIN_BARE_ONLY_SCOPES.has(scope)) {
|
|
45037
|
+
const action = collapseBareOnlyScopeRules({
|
|
45038
|
+
category,
|
|
45039
|
+
scope,
|
|
45040
|
+
rules,
|
|
45041
|
+
logger
|
|
45042
|
+
});
|
|
45043
|
+
if (action !== void 0) push(scope, action);
|
|
45044
|
+
continue;
|
|
44025
45045
|
}
|
|
45046
|
+
for (const [pattern, action] of Object.entries(rules)) push(buildDevinPermissionEntry(scope, pattern), action);
|
|
44026
45047
|
}
|
|
44027
45048
|
return {
|
|
44028
45049
|
allow,
|
|
@@ -46268,20 +47289,14 @@ const OPENCODE_ACTION_ONLY_PERMISSION_KEYS = /* @__PURE__ */ new Set([
|
|
|
46268
47289
|
"question",
|
|
46269
47290
|
"doom_loop"
|
|
46270
47291
|
]);
|
|
46271
|
-
const PERMISSION_ACTION_PRIORITY = {
|
|
46272
|
-
allow: 0,
|
|
46273
|
-
ask: 1,
|
|
46274
|
-
deny: 2
|
|
46275
|
-
};
|
|
46276
47292
|
function toOpencodePermission({ category, value, logger }) {
|
|
46277
47293
|
if (typeof value === "string" || !OPENCODE_ACTION_ONLY_PERMISSION_KEYS.has(category)) return value;
|
|
46278
|
-
const
|
|
46279
|
-
if (
|
|
47294
|
+
const action = collapseRulesToSingleAction({ rules: value });
|
|
47295
|
+
if (action === void 0) {
|
|
46280
47296
|
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.`);
|
|
46281
47297
|
return "deny";
|
|
46282
47298
|
}
|
|
46283
|
-
|
|
46284
|
-
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.`);
|
|
47299
|
+
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.`);
|
|
46285
47300
|
return action;
|
|
46286
47301
|
}
|
|
46287
47302
|
/**
|
|
@@ -46659,6 +47674,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
46659
47674
|
"visible",
|
|
46660
47675
|
"eager",
|
|
46661
47676
|
"listDirectory",
|
|
47677
|
+
"todoWrite",
|
|
46662
47678
|
"workflowsEnabled"
|
|
46663
47679
|
];
|
|
46664
47680
|
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
@@ -46764,6 +47780,11 @@ const QWEN_OVERRIDE_GROUPS = [{
|
|
|
46764
47780
|
rule: "global-machine-wide",
|
|
46765
47781
|
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.`,
|
|
46766
47782
|
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."
|
|
47783
|
+
},
|
|
47784
|
+
todoWrite: {
|
|
47785
|
+
rule: "global-machine-wide",
|
|
47786
|
+
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.`,
|
|
47787
|
+
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."
|
|
46767
47788
|
}
|
|
46768
47789
|
}
|
|
46769
47790
|
}, {
|
|
@@ -48570,9 +49591,25 @@ function widenToPrefixGlob(pattern) {
|
|
|
48570
49591
|
function toShellEntry(prefix) {
|
|
48571
49592
|
return prefix === "" ? SHELL_TOOL_NAME : `${SHELL_TOOL_NAME}(${prefix})`;
|
|
48572
49593
|
}
|
|
48573
|
-
/**
|
|
49594
|
+
/**
|
|
49595
|
+
* The `bash` pattern a `run_shell_command(<prefix>)` entry stands for. A
|
|
49596
|
+
* prefix Tabnine matches literally, so one an author spelled glob-like
|
|
49597
|
+
* (`git *`, `*`) is read as the glob it looks like rather than doubled into
|
|
49598
|
+
* `git * *`, which no comparison or warning could make sense of.
|
|
49599
|
+
*/
|
|
48574
49600
|
function toShellPattern(prefix) {
|
|
48575
|
-
|
|
49601
|
+
if (prefix === void 0 || prefix === "" || prefix === "*") return "*";
|
|
49602
|
+
return prefix.endsWith(" *") ? prefix : `${prefix} *`;
|
|
49603
|
+
}
|
|
49604
|
+
/**
|
|
49605
|
+
* Whether {@link toShellPattern} read the prefix as a glob: such an entry
|
|
49606
|
+
* would never fire on Tabnine as spelled (the prefix is matched literally, so
|
|
49607
|
+
* `run_shell_command(git *)` only matches a command line starting with the
|
|
49608
|
+
* two characters `git *`), and rulesync writes it back as the prefix the glob
|
|
49609
|
+
* denotes — a wider entry that the author is told about.
|
|
49610
|
+
*/
|
|
49611
|
+
function isGlobSpelledPrefix(prefix) {
|
|
49612
|
+
return prefix !== void 0 && (prefix === "*" || prefix.endsWith(" *"));
|
|
48576
49613
|
}
|
|
48577
49614
|
/**
|
|
48578
49615
|
* Split a `tools.allowed`/`tools.exclude` entry into its tool name and the
|
|
@@ -48698,7 +49735,7 @@ function collectTrustAffectingOverrideEntries({ tools, general }) {
|
|
|
48698
49735
|
const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
|
|
48699
49736
|
if (allowed.length > 0) entries.push({
|
|
48700
49737
|
label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
|
|
48701
|
-
reason: "auto-approves what it names as the override spells it; a non-shell entry
|
|
49738
|
+
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"
|
|
48702
49739
|
});
|
|
48703
49740
|
if (Object.hasOwn(tools, SANDBOX_KEY) && isNotTrue(tools[SANDBOX_KEY])) entries.push({
|
|
48704
49741
|
label: `tools.sandbox = ${quoteValueForWarning(tools[SANDBOX_KEY])}`,
|
|
@@ -48809,6 +49846,54 @@ function buildToolLists({ permission, overrideShellAllowPatterns, logger }) {
|
|
|
48809
49846
|
};
|
|
48810
49847
|
}
|
|
48811
49848
|
/**
|
|
49849
|
+
* Names the glob-spelled `run_shell_command(...)` override entries that were
|
|
49850
|
+
* written as the prefix their glob denotes. Said only for the entries that
|
|
49851
|
+
* made it into the list: one that a deny or ask overlaps is announced as
|
|
49852
|
+
* withheld by the comparison instead, and one whose glob names no prefix at
|
|
49853
|
+
* all is skipped by it.
|
|
49854
|
+
*/
|
|
49855
|
+
function warnAboutGlobSpelledOverrideEntries({ entries, allowed, logger }) {
|
|
49856
|
+
for (const { entry, pattern } of entries) {
|
|
49857
|
+
const prefix = toShellPrefix(pattern);
|
|
49858
|
+
const written = prefix === void 0 ? void 0 : toShellEntry(prefix);
|
|
49859
|
+
if (written === void 0 || !allowed.includes(written)) continue;
|
|
49860
|
+
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.`);
|
|
49861
|
+
}
|
|
49862
|
+
}
|
|
49863
|
+
/**
|
|
49864
|
+
* A verbatim override allow naming a tool the canonical block denies is
|
|
49865
|
+
* withheld the way a shadowed `bash` allow is: Tabnine never loads an excluded
|
|
49866
|
+
* tool whatever `allowed` says, so the entry would only ever contradict the
|
|
49867
|
+
* deny it sits beside — and an override is not a way around a canonical rule.
|
|
49868
|
+
* Returns the entries that may be written.
|
|
49869
|
+
*/
|
|
49870
|
+
function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logger }) {
|
|
49871
|
+
const denied = overrideVerbatimAllowed.filter((entry) => {
|
|
49872
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
49873
|
+
return toolName !== void 0 && exclude.includes(toolName);
|
|
49874
|
+
});
|
|
49875
|
+
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.`);
|
|
49876
|
+
return overrideVerbatimAllowed.filter((entry) => !denied.includes(entry));
|
|
49877
|
+
}
|
|
49878
|
+
/**
|
|
49879
|
+
* The entries of a managed tool that the canonical block did not re-derive are
|
|
49880
|
+
* named rather than removed silently. Dropping an exclude loosens the policy,
|
|
49881
|
+
* so it is a warning; dropping an allow only brings back Tabnine's confirmation
|
|
49882
|
+
* prompt, so it is said at info level for the author who wonders where a
|
|
49883
|
+
* hand-written entry went.
|
|
49884
|
+
*/
|
|
49885
|
+
function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
|
|
49886
|
+
const dropped = (key, list) => (0, es_toolkit.uniq)(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
|
|
49887
|
+
if (list.includes(entry)) return false;
|
|
49888
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
49889
|
+
return toolName === void 0 || !list.includes(toolName);
|
|
49890
|
+
});
|
|
49891
|
+
const droppedExclude = dropped(EXCLUDE_KEY, excludeList);
|
|
49892
|
+
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.`);
|
|
49893
|
+
const droppedAllowed = dropped(ALLOWED_KEY, allowedList);
|
|
49894
|
+
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.`);
|
|
49895
|
+
}
|
|
49896
|
+
/**
|
|
48812
49897
|
* Permissions generator for the Tabnine CLI.
|
|
48813
49898
|
*
|
|
48814
49899
|
* Tabnine CLI keeps its tool policy in `.tabnine/agent/settings.json` (project)
|
|
@@ -48874,7 +49959,10 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48874
49959
|
const config = rulesyncPermissions.getJson();
|
|
48875
49960
|
const override = isRecord$1(config.tabnine) ? config.tabnine : {};
|
|
48876
49961
|
const { tools: overrideTools, general: overrideGeneral, refused: refusedPaths } = stripRefusedOverridePaths(override);
|
|
48877
|
-
if (refusedPaths.length > 0)
|
|
49962
|
+
if (refusedPaths.length > 0) {
|
|
49963
|
+
const one = refusedPaths.length === 1;
|
|
49964
|
+
warnWithFallback(logger, `Tabnine CLI permissions: refused to write ${refusedPaths.join(", ")} from the tabnine override; Tabnine CLI runs ${one ? "that value" : "those values"} as a command or sends its traffic there, and a permissions file (one that came from 'rulesync fetch' included) must not be able to point it at an executable or a server of its choosing. Set ${one ? "it" : "them"} by hand in ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} if you need ${one ? "it" : "them"}.`);
|
|
49965
|
+
}
|
|
48878
49966
|
const overrideList = (key) => {
|
|
48879
49967
|
const value = overrideTools[key];
|
|
48880
49968
|
if (value === void 0 || isStringArray$2(value)) return value ?? [];
|
|
@@ -48883,10 +49971,17 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48883
49971
|
};
|
|
48884
49972
|
const overrideShellAllowPatterns = [];
|
|
48885
49973
|
const overrideVerbatimAllowed = [];
|
|
49974
|
+
const globSpelledOverrideEntries = [];
|
|
48886
49975
|
for (const entry of overrideList(ALLOWED_KEY)) {
|
|
48887
49976
|
const parsed = parseTabnineEntry(entry);
|
|
48888
|
-
if (parsed?.toolName === SHELL_TOOL_NAME)
|
|
48889
|
-
|
|
49977
|
+
if (parsed?.toolName === SHELL_TOOL_NAME) {
|
|
49978
|
+
const pattern = toShellPattern(parsed.prefix);
|
|
49979
|
+
if (isGlobSpelledPrefix(parsed.prefix)) globSpelledOverrideEntries.push({
|
|
49980
|
+
entry,
|
|
49981
|
+
pattern
|
|
49982
|
+
});
|
|
49983
|
+
overrideShellAllowPatterns.push(pattern);
|
|
49984
|
+
} else overrideVerbatimAllowed.push(entry);
|
|
48890
49985
|
}
|
|
48891
49986
|
const overrideExclude = overrideList(EXCLUDE_KEY);
|
|
48892
49987
|
const { allowed, exclude } = buildToolLists({
|
|
@@ -48894,6 +49989,16 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48894
49989
|
overrideShellAllowPatterns,
|
|
48895
49990
|
logger
|
|
48896
49991
|
});
|
|
49992
|
+
warnAboutGlobSpelledOverrideEntries({
|
|
49993
|
+
entries: globSpelledOverrideEntries,
|
|
49994
|
+
allowed,
|
|
49995
|
+
logger
|
|
49996
|
+
});
|
|
49997
|
+
const writableOverrideAllowed = withholdDeniedOverrideAllowed({
|
|
49998
|
+
overrideVerbatimAllowed,
|
|
49999
|
+
exclude,
|
|
50000
|
+
logger
|
|
50001
|
+
});
|
|
48897
50002
|
const ignoredOverrideKeys = Object.keys(override).filter((key) => key !== TOOLS_KEY && key !== GENERAL_KEY);
|
|
48898
50003
|
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.`);
|
|
48899
50004
|
const existingTools = this.existingToolsGroup(existingContent);
|
|
@@ -48904,7 +50009,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48904
50009
|
});
|
|
48905
50010
|
const allowedList = (0, es_toolkit.uniq)([
|
|
48906
50011
|
...allowed,
|
|
48907
|
-
...
|
|
50012
|
+
...writableOverrideAllowed,
|
|
48908
50013
|
...preservedEntries(ALLOWED_KEY)
|
|
48909
50014
|
]);
|
|
48910
50015
|
const excludeList = (0, es_toolkit.uniq)([
|
|
@@ -48912,12 +50017,12 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48912
50017
|
...overrideExclude,
|
|
48913
50018
|
...preservedEntries(EXCLUDE_KEY)
|
|
48914
50019
|
]);
|
|
48915
|
-
|
|
48916
|
-
|
|
48917
|
-
|
|
48918
|
-
|
|
50020
|
+
reportDroppedManagedEntries({
|
|
50021
|
+
existingTools,
|
|
50022
|
+
allowedList,
|
|
50023
|
+
excludeList,
|
|
50024
|
+
logger
|
|
48919
50025
|
});
|
|
48920
|
-
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.`);
|
|
48921
50026
|
const tools = {
|
|
48922
50027
|
...overrideTools,
|
|
48923
50028
|
[ALLOWED_KEY]: allowedList.length > 0 ? allowedList : void 0,
|
|
@@ -48931,7 +50036,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48931
50036
|
entries: collectTrustAffectingOverrideEntries({
|
|
48932
50037
|
tools: {
|
|
48933
50038
|
...overrideTools,
|
|
48934
|
-
[ALLOWED_KEY]:
|
|
50039
|
+
[ALLOWED_KEY]: writableOverrideAllowed
|
|
48935
50040
|
},
|
|
48936
50041
|
general: overrideGeneral
|
|
48937
50042
|
}),
|
|
@@ -52288,6 +53393,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52288
53393
|
supportsImport: true
|
|
52289
53394
|
}
|
|
52290
53395
|
}],
|
|
53396
|
+
["crush", {
|
|
53397
|
+
class: CrushPermissions,
|
|
53398
|
+
meta: {
|
|
53399
|
+
supportsProject: true,
|
|
53400
|
+
supportsGlobal: true,
|
|
53401
|
+
supportsImport: true
|
|
53402
|
+
}
|
|
53403
|
+
}],
|
|
52291
53404
|
["cursor", {
|
|
52292
53405
|
class: CursorPermissions,
|
|
52293
53406
|
meta: {
|
|
@@ -58309,6 +59422,183 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
58309
59422
|
}
|
|
58310
59423
|
};
|
|
58311
59424
|
//#endregion
|
|
59425
|
+
//#region src/constants/pool-paths.ts
|
|
59426
|
+
/**
|
|
59427
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
59428
|
+
*
|
|
59429
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
59430
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
59431
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
59432
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
59433
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
59434
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
59435
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
59436
|
+
*
|
|
59437
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
59438
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
59439
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
59440
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
59441
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
59442
|
+
*
|
|
59443
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
59444
|
+
* @see https://docs.poolside.ai/skills
|
|
59445
|
+
* @see https://github.com/poolsideai/pool
|
|
59446
|
+
*/
|
|
59447
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
59448
|
+
const POOL_DIR = ".poolside";
|
|
59449
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
59450
|
+
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
59451
|
+
/** Project skills root, relative to the project root. */
|
|
59452
|
+
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
59453
|
+
/** Global skills root, relative to the home directory. */
|
|
59454
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
59455
|
+
//#endregion
|
|
59456
|
+
//#region src/features/skills/pool-skill.ts
|
|
59457
|
+
const PoolSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
59458
|
+
name: zod_mini.z.string(),
|
|
59459
|
+
description: zod_mini.z.string()
|
|
59460
|
+
});
|
|
59461
|
+
/**
|
|
59462
|
+
* Represents a Pool (Poolside) skill directory.
|
|
59463
|
+
*
|
|
59464
|
+
* Pool discovers Agent Skills (`<name>/SKILL.md` bundles, optionally with
|
|
59465
|
+
* supporting files) from `.poolside/skills/` (project) and
|
|
59466
|
+
* `~/.config/poolside/skills/` (global). It also scans the shared
|
|
59467
|
+
* `.agents/skills/` / `~/.agents/skills/` roots and the skill directories of
|
|
59468
|
+
* other Agent Skills tools; those belong to their own targets, so this class
|
|
59469
|
+
* writes only the two Pool-specific roots and a skill is written exactly once.
|
|
59470
|
+
* Pool requires the directory name to equal the frontmatter `name`, otherwise
|
|
59471
|
+
* it skips the skill. Unlike adapters that reject such a skill outright, this
|
|
59472
|
+
* one warns and still writes it: the directory name is the canonical identity
|
|
59473
|
+
* shared with every other target, and Pool merely skips the skill rather than
|
|
59474
|
+
* failing, so a hard error here would block the other targets for a
|
|
59475
|
+
* Pool-only concern. Import is lenient for the same reason.
|
|
59476
|
+
* @see https://docs.poolside.ai/skills
|
|
59477
|
+
*/
|
|
59478
|
+
var PoolSkill = class PoolSkill extends ToolSkill {
|
|
59479
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = POOL_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
59480
|
+
super({
|
|
59481
|
+
outputRoot,
|
|
59482
|
+
relativeDirPath,
|
|
59483
|
+
dirName,
|
|
59484
|
+
mainFile: {
|
|
59485
|
+
name: SKILL_FILE_NAME,
|
|
59486
|
+
body,
|
|
59487
|
+
frontmatter: { ...frontmatter }
|
|
59488
|
+
},
|
|
59489
|
+
otherFiles,
|
|
59490
|
+
global
|
|
59491
|
+
});
|
|
59492
|
+
if (validate) {
|
|
59493
|
+
const result = this.validate();
|
|
59494
|
+
if (!result.success) throw result.error;
|
|
59495
|
+
}
|
|
59496
|
+
}
|
|
59497
|
+
static getSettablePaths({ global = false } = {}) {
|
|
59498
|
+
return { relativeDirPath: global ? POOL_GLOBAL_SKILLS_DIR_PATH : POOL_SKILLS_DIR_PATH };
|
|
59499
|
+
}
|
|
59500
|
+
getFrontmatter() {
|
|
59501
|
+
return PoolSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
59502
|
+
}
|
|
59503
|
+
getBody() {
|
|
59504
|
+
return this.mainFile?.body ?? "";
|
|
59505
|
+
}
|
|
59506
|
+
validate() {
|
|
59507
|
+
if (!this.mainFile) return {
|
|
59508
|
+
success: false,
|
|
59509
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
59510
|
+
};
|
|
59511
|
+
const result = PoolSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
59512
|
+
if (!result.success) return {
|
|
59513
|
+
success: false,
|
|
59514
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
59515
|
+
};
|
|
59516
|
+
return {
|
|
59517
|
+
success: true,
|
|
59518
|
+
error: null
|
|
59519
|
+
};
|
|
59520
|
+
}
|
|
59521
|
+
toRulesyncSkill() {
|
|
59522
|
+
const frontmatter = this.getFrontmatter();
|
|
59523
|
+
const rulesyncFrontmatter = {
|
|
59524
|
+
name: frontmatter.name,
|
|
59525
|
+
description: frontmatter.description,
|
|
59526
|
+
targets: ["*"]
|
|
59527
|
+
};
|
|
59528
|
+
return new RulesyncSkill({
|
|
59529
|
+
outputRoot: this.outputRoot,
|
|
59530
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
59531
|
+
dirName: this.getDirName(),
|
|
59532
|
+
frontmatter: rulesyncFrontmatter,
|
|
59533
|
+
body: this.getBody(),
|
|
59534
|
+
otherFiles: this.getOtherFiles(),
|
|
59535
|
+
validate: true,
|
|
59536
|
+
global: this.global
|
|
59537
|
+
});
|
|
59538
|
+
}
|
|
59539
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
59540
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
59541
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
59542
|
+
const dirName = rulesyncSkill.getDirName();
|
|
59543
|
+
const poolFrontmatter = {
|
|
59544
|
+
name: rulesyncFrontmatter.name,
|
|
59545
|
+
description: rulesyncFrontmatter.description
|
|
59546
|
+
};
|
|
59547
|
+
if (poolFrontmatter.name !== dirName) warnWithFallback(logger, `${stripControlCharacters(toPosixPath((0, node_path.join)(outputRoot, settablePaths.relativeDirPath, dirName, SKILL_FILE_NAME)))}: \`name\` ${quoteForLog(poolFrontmatter.name)} does not match its directory name ${quoteForLog(dirName)}; Pool only loads a skill whose directory name equals its \`name\``);
|
|
59548
|
+
return new PoolSkill({
|
|
59549
|
+
outputRoot,
|
|
59550
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
59551
|
+
dirName,
|
|
59552
|
+
frontmatter: poolFrontmatter,
|
|
59553
|
+
body: rulesyncSkill.getBody(),
|
|
59554
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
59555
|
+
validate,
|
|
59556
|
+
global
|
|
59557
|
+
});
|
|
59558
|
+
}
|
|
59559
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
59560
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
59561
|
+
return targets.includes("*") || targets.includes("pool");
|
|
59562
|
+
}
|
|
59563
|
+
static async fromDir(params) {
|
|
59564
|
+
const loaded = await this.loadSkillDirContent({
|
|
59565
|
+
...params,
|
|
59566
|
+
getSettablePaths: PoolSkill.getSettablePaths
|
|
59567
|
+
});
|
|
59568
|
+
const result = PoolSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
59569
|
+
if (!result.success) {
|
|
59570
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
59571
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
59572
|
+
}
|
|
59573
|
+
return new PoolSkill({
|
|
59574
|
+
outputRoot: loaded.outputRoot,
|
|
59575
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
59576
|
+
dirName: loaded.dirName,
|
|
59577
|
+
frontmatter: result.data,
|
|
59578
|
+
body: loaded.body,
|
|
59579
|
+
otherFiles: loaded.otherFiles,
|
|
59580
|
+
validate: true,
|
|
59581
|
+
global: loaded.global
|
|
59582
|
+
});
|
|
59583
|
+
}
|
|
59584
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
59585
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
59586
|
+
return new PoolSkill({
|
|
59587
|
+
outputRoot,
|
|
59588
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
59589
|
+
dirName,
|
|
59590
|
+
frontmatter: {
|
|
59591
|
+
name: "",
|
|
59592
|
+
description: ""
|
|
59593
|
+
},
|
|
59594
|
+
body: "",
|
|
59595
|
+
otherFiles: [],
|
|
59596
|
+
validate: false,
|
|
59597
|
+
global
|
|
59598
|
+
});
|
|
59599
|
+
}
|
|
59600
|
+
};
|
|
59601
|
+
//#endregion
|
|
58312
59602
|
//#region src/features/skills/qwencode-skill.ts
|
|
58313
59603
|
const QwencodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
58314
59604
|
name: zod_mini.z.string(),
|
|
@@ -60195,6 +61485,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60195
61485
|
supportsGlobal: true
|
|
60196
61486
|
}
|
|
60197
61487
|
}],
|
|
61488
|
+
["pool", {
|
|
61489
|
+
class: PoolSkill,
|
|
61490
|
+
meta: {
|
|
61491
|
+
supportsProject: true,
|
|
61492
|
+
supportsSimulated: false,
|
|
61493
|
+
supportsGlobal: true
|
|
61494
|
+
}
|
|
61495
|
+
}],
|
|
60198
61496
|
["qwencode", {
|
|
60199
61497
|
class: QwencodeSkill,
|
|
60200
61498
|
meta: {
|
|
@@ -65029,7 +66327,7 @@ var RooSubagent = class extends ToolSubagent {
|
|
|
65029
66327
|
//#region src/features/subagents/tabnine-subagent.ts
|
|
65030
66328
|
const TabnineSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
65031
66329
|
name: zod_mini.z.string(),
|
|
65032
|
-
description: zod_mini.z.
|
|
66330
|
+
description: zod_mini.z.string(),
|
|
65033
66331
|
/** "local" (default) runs inside the CLI; "remote" delegates to a Tabnine cloud agent. */
|
|
65034
66332
|
kind: zod_mini.z.optional(zod_mini.z.string()),
|
|
65035
66333
|
/** Built-in tool names the subagent may use; omitted = every tool. */
|
|
@@ -65088,14 +66386,23 @@ var TabnineSubagent = class TabnineSubagent extends ToolSubagent {
|
|
|
65088
66386
|
validate: true
|
|
65089
66387
|
});
|
|
65090
66388
|
}
|
|
65091
|
-
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
66389
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
65092
66390
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
65093
66391
|
const tabnineSection = rulesyncFrontmatter.tabnine ?? {};
|
|
65094
|
-
const
|
|
66392
|
+
const merged = {
|
|
65095
66393
|
name: rulesyncFrontmatter.name,
|
|
65096
66394
|
description: rulesyncFrontmatter.description,
|
|
65097
66395
|
...tabnineSection
|
|
65098
66396
|
};
|
|
66397
|
+
let description = merged.description;
|
|
66398
|
+
if (description === void 0 || description === "") {
|
|
66399
|
+
description = rulesyncFrontmatter.name ? `${rulesyncFrontmatter.name} subagent` : "subagent";
|
|
66400
|
+
logger?.warn(`Tabnine CLI subagent ${rulesyncSubagent.getRelativeFilePath()} has no description, which Tabnine requires; wrote ${JSON.stringify(description)} as a placeholder.`);
|
|
66401
|
+
}
|
|
66402
|
+
const tabnineSubagentFrontmatter = {
|
|
66403
|
+
...merged,
|
|
66404
|
+
description
|
|
66405
|
+
};
|
|
65099
66406
|
const body = rulesyncSubagent.getBody();
|
|
65100
66407
|
const fileContent = stringifyFrontmatter(body, tabnineSubagentFrontmatter, { avoidBlockScalars: true });
|
|
65101
66408
|
const paths = this.getSettablePaths({ global });
|
|
@@ -71358,24 +72665,6 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
71358
72665
|
}
|
|
71359
72666
|
};
|
|
71360
72667
|
//#endregion
|
|
71361
|
-
//#region src/constants/pool-paths.ts
|
|
71362
|
-
/**
|
|
71363
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
71364
|
-
*
|
|
71365
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
71366
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
71367
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
71368
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
71369
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
71370
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
71371
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
71372
|
-
*
|
|
71373
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
71374
|
-
* @see https://github.com/poolsideai/pool
|
|
71375
|
-
*/
|
|
71376
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
71377
|
-
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
71378
|
-
//#endregion
|
|
71379
72668
|
//#region src/features/rules/pool-rule.ts
|
|
71380
72669
|
var PoolRule = class PoolRule extends ToolRule {
|
|
71381
72670
|
static getSettablePaths({ global = false } = {}) {
|