rulesync 16.32.1 → 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-CT7-pKL6.js → import-C6R-lXFF.js} +1838 -559
- package/dist/import-C6R-lXFF.js.map +1 -0
- package/dist/{import-NHZnMc--.cjs → import-DVRU6cLB.cjs} +1853 -574
- 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-CT7-pKL6.js.map +0 -1
|
@@ -558,6 +558,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
558
558
|
"copilot",
|
|
559
559
|
"copilotcli",
|
|
560
560
|
"cortexcode",
|
|
561
|
+
"crush",
|
|
561
562
|
"cursor",
|
|
562
563
|
"deepagents",
|
|
563
564
|
"factorydroid",
|
|
@@ -694,6 +695,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
694
695
|
"musecode",
|
|
695
696
|
"opencode",
|
|
696
697
|
"pi",
|
|
698
|
+
"pool",
|
|
697
699
|
"qwencode",
|
|
698
700
|
"reasonix",
|
|
699
701
|
"replit",
|
|
@@ -724,6 +726,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
724
726
|
"copilot",
|
|
725
727
|
"copilotcli",
|
|
726
728
|
"cortexcode",
|
|
729
|
+
"crush",
|
|
727
730
|
"opencode",
|
|
728
731
|
"pi",
|
|
729
732
|
"factorydroid",
|
|
@@ -756,6 +759,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
756
759
|
"continue",
|
|
757
760
|
"copilot",
|
|
758
761
|
"copilotcli",
|
|
762
|
+
"crush",
|
|
759
763
|
"cursor",
|
|
760
764
|
"deepagents",
|
|
761
765
|
"devin",
|
|
@@ -3283,6 +3287,9 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3283
3287
|
"reasonix.toml",
|
|
3284
3288
|
".vscode/settings.json",
|
|
3285
3289
|
".zed/settings.json",
|
|
3290
|
+
"crush.json",
|
|
3291
|
+
".crush.json",
|
|
3292
|
+
".config/crush/crush.json",
|
|
3286
3293
|
"kilo.json",
|
|
3287
3294
|
"kilo.jsonc",
|
|
3288
3295
|
"opencode.json",
|
|
@@ -3487,6 +3494,27 @@ function omitPrototypePollutionKeys(record) {
|
|
|
3487
3494
|
}
|
|
3488
3495
|
return sanitized;
|
|
3489
3496
|
}
|
|
3497
|
+
/**
|
|
3498
|
+
* Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
|
|
3499
|
+
* reachable through the value (directly, or through an array) is copied with
|
|
3500
|
+
* its prototype-pollution keys dropped. Scalars and arrays of scalars come back
|
|
3501
|
+
* as they are.
|
|
3502
|
+
*
|
|
3503
|
+
* Use when passing an unknown, user-supplied value through unchanged — an MCP
|
|
3504
|
+
* server field a tool documents that rulesync does not model — so a
|
|
3505
|
+
* `__proto__` key nested anywhere inside it cannot ride into the generated
|
|
3506
|
+
* config, where the consuming tool may merge the object without the same care.
|
|
3507
|
+
*/
|
|
3508
|
+
function omitPrototypePollutionKeysDeep(value) {
|
|
3509
|
+
if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
|
|
3510
|
+
if (value === null || typeof value !== "object") return value;
|
|
3511
|
+
const sanitized = {};
|
|
3512
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3513
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3514
|
+
sanitized[key] = omitPrototypePollutionKeysDeep(entry);
|
|
3515
|
+
}
|
|
3516
|
+
return sanitized;
|
|
3517
|
+
}
|
|
3490
3518
|
//#endregion
|
|
3491
3519
|
//#region src/utils/type-guards.ts
|
|
3492
3520
|
/**
|
|
@@ -4554,6 +4582,14 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4554
4582
|
"stop"
|
|
4555
4583
|
];
|
|
4556
4584
|
/**
|
|
4585
|
+
* Hook events supported by Crush.
|
|
4586
|
+
*
|
|
4587
|
+
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
4588
|
+
* (Claude Code-compatible payload; `matcher` is a regex on the tool name).
|
|
4589
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
4590
|
+
*/
|
|
4591
|
+
const CRUSH_HOOK_EVENTS = ["preToolUse"];
|
|
4592
|
+
/**
|
|
4557
4593
|
* Hook events supported by JetBrains Junie CLI.
|
|
4558
4594
|
*
|
|
4559
4595
|
* Junie CLI exposes seven lifecycle events under the `"hooks"` key of
|
|
@@ -5096,6 +5132,7 @@ const HooksConfigSchema = z.looseObject({
|
|
|
5096
5132
|
grokcli: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5097
5133
|
"kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5098
5134
|
zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5135
|
+
crush: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5099
5136
|
bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5100
5137
|
cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5101
5138
|
continue: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
@@ -5593,6 +5630,13 @@ const VIBE_TO_CANONICAL_EVENT_NAMES = {
|
|
|
5593
5630
|
post_agent_turn: "stop"
|
|
5594
5631
|
};
|
|
5595
5632
|
/**
|
|
5633
|
+
* Canonical -> Crush event names. Crush spells its events the Claude Code
|
|
5634
|
+
* way (`PreToolUse`).
|
|
5635
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
5636
|
+
*/
|
|
5637
|
+
const CANONICAL_TO_CRUSH_EVENT_NAMES = { preToolUse: "PreToolUse" };
|
|
5638
|
+
const CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5639
|
+
/**
|
|
5596
5640
|
* Map canonical camelCase event names to Qwen Code PascalCase.
|
|
5597
5641
|
*
|
|
5598
5642
|
* Qwen Code reuses the same Claude-style PascalCase event names for the events
|
|
@@ -6032,6 +6076,11 @@ const McpServerSchema = z.looseObject({
|
|
|
6032
6076
|
kiroAutoBlock: z.optional(z.array(z.string())),
|
|
6033
6077
|
musecodeMode: z.optional(z.enum(["required", "optional"])),
|
|
6034
6078
|
rovodevEnableInstructions: z.optional(z.boolean()),
|
|
6079
|
+
crushOauth: z.optional(z.boolean()),
|
|
6080
|
+
crushOauthClientId: z.optional(z.string()),
|
|
6081
|
+
crushOauthClientSecret: z.optional(z.string()),
|
|
6082
|
+
crushOauthCallbackPort: z.optional(z.int()),
|
|
6083
|
+
crushSessionless: z.optional(z.boolean()),
|
|
6035
6084
|
headers: z.optional(z.record(z.string(), z.string())),
|
|
6036
6085
|
/**
|
|
6037
6086
|
* The canonical per-server tool allowlist.
|
|
@@ -6108,6 +6157,8 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
6108
6157
|
continue: z.optional(toolScopedMcpSchema),
|
|
6109
6158
|
copilot: z.optional(toolScopedMcpSchema),
|
|
6110
6159
|
copilotcli: z.optional(toolScopedMcpSchema),
|
|
6160
|
+
cortexcode: z.optional(toolScopedMcpSchema),
|
|
6161
|
+
crush: z.optional(toolScopedMcpSchema),
|
|
6111
6162
|
cursor: z.optional(toolScopedMcpSchema),
|
|
6112
6163
|
deepagents: z.optional(toolScopedMcpSchema),
|
|
6113
6164
|
devin: z.optional(toolScopedMcpSchema),
|
|
@@ -6124,7 +6175,6 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
6124
6175
|
reasonix: z.optional(toolScopedMcpSchema),
|
|
6125
6176
|
roo: z.optional(toolScopedMcpSchema),
|
|
6126
6177
|
rovodev: z.optional(toolScopedMcpSchema),
|
|
6127
|
-
cortexcode: z.optional(toolScopedMcpSchema),
|
|
6128
6178
|
tabnine: z.optional(toolScopedMcpSchema),
|
|
6129
6179
|
takt: z.optional(toolScopedMcpSchema),
|
|
6130
6180
|
vibe: z.optional(toolScopedMcpSchema),
|
|
@@ -6456,6 +6506,15 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6456
6506
|
"musecodeMode",
|
|
6457
6507
|
"rovodevEnableInstructions",
|
|
6458
6508
|
"enable_instructions",
|
|
6509
|
+
"crushOauth",
|
|
6510
|
+
"crushOauthClientId",
|
|
6511
|
+
"crushOauthClientSecret",
|
|
6512
|
+
"crushOauthCallbackPort",
|
|
6513
|
+
"crushSessionless",
|
|
6514
|
+
"oauth_client_id",
|
|
6515
|
+
"oauth_client_secret",
|
|
6516
|
+
"oauth_callback_port",
|
|
6517
|
+
"sessionless",
|
|
6459
6518
|
"enabled"
|
|
6460
6519
|
])];
|
|
6461
6520
|
}));
|
|
@@ -6904,7 +6963,7 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
6904
6963
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
6905
6964
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
6906
6965
|
* `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
|
|
6907
|
-
* `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
6966
|
+
* `todoWrite`, `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
|
|
6908
6967
|
* `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
|
|
6909
6968
|
* `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
|
|
6910
6969
|
* `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
|
|
@@ -6917,7 +6976,7 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
6917
6976
|
* changes what the file said is reported in either scope, naming what that key
|
|
6918
6977
|
* decides there — the autonomy and containment
|
|
6919
6978
|
* controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
|
|
6920
|
-
* registry controls (`disabled`, `visible`, `listDirectory`, and `eager`, which
|
|
6979
|
+
* registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
|
|
6921
6980
|
* demotes an omitted tool to deferred rather than removing it, replacing rather
|
|
6922
6981
|
* than merging with the list a higher scope set), the Auto Mode
|
|
6923
6982
|
* classifier config, and, because these groups are loose objects, any key
|
|
@@ -6958,6 +7017,14 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
|
6958
7017
|
* `looseObject` (verbatim passthrough). Both project and global scope are
|
|
6959
7018
|
* supported.
|
|
6960
7019
|
*
|
|
7020
|
+
* Only `tools` and `general` are written; any other key is ignored with a
|
|
7021
|
+
* warning. Within them, the paths Tabnine CLI runs as a command or sends its
|
|
7022
|
+
* traffic to — `tools.discoveryCommand`, `tools.callCommand`,
|
|
7023
|
+
* `tools.shell.pager`, `tools.sandbox.command`, `general.tabnineHost` and
|
|
7024
|
+
* `general.preferredEditor` — are refused with a warning rather than written,
|
|
7025
|
+
* so a fetched permissions file cannot point the CLI at an executable or a
|
|
7026
|
+
* server of its choosing; set those by hand in `settings.json`.
|
|
7027
|
+
*
|
|
6961
7028
|
* @example
|
|
6962
7029
|
* { "tools": { "core": ["read_file", "run_shell_command(git)"] },
|
|
6963
7030
|
* "general": { "defaultApprovalMode": "plan" } }
|
|
@@ -7742,6 +7809,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
7742
7809
|
continue: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7743
7810
|
copilot: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7744
7811
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7812
|
+
crush: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7745
7813
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7746
7814
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7747
7815
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
@@ -12767,6 +12835,26 @@ const ZCODE_USER_CONFIG_DECLARATION = {
|
|
|
12767
12835
|
}
|
|
12768
12836
|
};
|
|
12769
12837
|
/**
|
|
12838
|
+
* Crush's JSON config, shared by the project (`crush.json` / `.crush.json`)
|
|
12839
|
+
* and global (`~/.config/crush/crush.json`) spellings so a policy edit lands
|
|
12840
|
+
* on both. See the `SHARED_CONFIG_OWNERSHIP` entries for the key rationale.
|
|
12841
|
+
*/
|
|
12842
|
+
const CRUSH_CONFIG_DECLARATION = {
|
|
12843
|
+
format: "json",
|
|
12844
|
+
invalidRootPolicy: "error",
|
|
12845
|
+
features: {
|
|
12846
|
+
mcp: {
|
|
12847
|
+
kind: "replace-owned-keys",
|
|
12848
|
+
ownedKeys: ["mcp"]
|
|
12849
|
+
},
|
|
12850
|
+
hooks: {
|
|
12851
|
+
kind: "replace-owned-keys",
|
|
12852
|
+
ownedKeys: ["hooks"]
|
|
12853
|
+
},
|
|
12854
|
+
permissions: { kind: "deep-merge" }
|
|
12855
|
+
}
|
|
12856
|
+
};
|
|
12857
|
+
/**
|
|
12770
12858
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
12771
12859
|
* and both validate against the one published schema, which the gateway
|
|
12772
12860
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13027,6 +13115,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13027
13115
|
},
|
|
13028
13116
|
".zcode/config.json": ZCODE_WORKSPACE_CONFIG_DECLARATION,
|
|
13029
13117
|
".zcode/cli/config.json": ZCODE_USER_CONFIG_DECLARATION,
|
|
13118
|
+
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13119
|
+
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13030
13120
|
".bob/settings.json": {
|
|
13031
13121
|
format: "json",
|
|
13032
13122
|
invalidRootPolicy: "error",
|
|
@@ -18876,15 +18966,12 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
|
|
|
18876
18966
|
return this.body;
|
|
18877
18967
|
}
|
|
18878
18968
|
getFrontmatter() {
|
|
18879
|
-
return
|
|
18880
|
-
description: this.frontmatter.description,
|
|
18881
|
-
prompt: this.frontmatter.prompt
|
|
18882
|
-
};
|
|
18969
|
+
return this.frontmatter;
|
|
18883
18970
|
}
|
|
18884
18971
|
toRulesyncCommand() {
|
|
18885
18972
|
const { description, prompt: _prompt, ...restFields } = this.frontmatter;
|
|
18886
18973
|
const rulesyncFrontmatter = {
|
|
18887
|
-
targets: ["
|
|
18974
|
+
targets: ["*"],
|
|
18888
18975
|
description,
|
|
18889
18976
|
...Object.keys(restFields).length > 0 && { tabnine: restFields }
|
|
18890
18977
|
};
|
|
@@ -20847,7 +20934,20 @@ function stableJson(value) {
|
|
|
20847
20934
|
*/
|
|
20848
20935
|
function isSafeStringRecord(value) {
|
|
20849
20936
|
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
20850
|
-
return Object.entries(value).every(([key, entry]) =>
|
|
20937
|
+
return Object.entries(value).every(([key, entry]) => isSafeEnvEntry({
|
|
20938
|
+
key,
|
|
20939
|
+
value: entry
|
|
20940
|
+
}));
|
|
20941
|
+
}
|
|
20942
|
+
/**
|
|
20943
|
+
* Whether one `env` entry survives the rule {@link isSafeStringRecord} applies
|
|
20944
|
+
* to the whole map. Exported for the adapters that hand a tool an environment
|
|
20945
|
+
* block outside the shared converter (Tabnine, whose hook shape it does not
|
|
20946
|
+
* express) so they drop the same entries this one does.
|
|
20947
|
+
*/
|
|
20948
|
+
function isSafeEnvEntry(entry) {
|
|
20949
|
+
const { key, value } = entry;
|
|
20950
|
+
return typeof value === "string" && key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || value.includes(char));
|
|
20851
20951
|
}
|
|
20852
20952
|
/**
|
|
20853
20953
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
@@ -21738,135 +21838,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
21738
21838
|
}
|
|
21739
21839
|
};
|
|
21740
21840
|
//#endregion
|
|
21741
|
-
//#region src/features/hooks/bob-hooks.ts
|
|
21742
|
-
const BOB_CONVERTER_CONFIG = {
|
|
21743
|
-
supportedEvents: BOB_HOOK_EVENTS,
|
|
21744
|
-
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
21745
|
-
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
21746
|
-
projectDirVar: "",
|
|
21747
|
-
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
21748
|
-
"sessionStart",
|
|
21749
|
-
"beforeSubmitPrompt",
|
|
21750
|
-
"stop"
|
|
21751
|
-
]),
|
|
21752
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
21753
|
-
wildcardMatcherMeansAll: true
|
|
21754
|
-
};
|
|
21755
|
-
/**
|
|
21756
|
-
* Single spelling of the settings.json codec/policy: fail closed on an
|
|
21757
|
-
* unparseable root rather than replacing the user's Bob settings with
|
|
21758
|
-
* generated output.
|
|
21759
|
-
*/
|
|
21760
|
-
function parseBobSettings(fileContent, filePath) {
|
|
21761
|
-
return parseSharedConfig({
|
|
21762
|
-
format: "json",
|
|
21763
|
-
fileContent,
|
|
21764
|
-
filePath,
|
|
21765
|
-
invalidRootPolicy: "error"
|
|
21766
|
-
});
|
|
21767
|
-
}
|
|
21768
|
-
/**
|
|
21769
|
-
* IBM Bob lifecycle hooks.
|
|
21770
|
-
*
|
|
21771
|
-
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
21772
|
-
* `<project>/.bob/settings.json` (project scope) and
|
|
21773
|
-
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
21774
|
-
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
21775
|
-
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
21776
|
-
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
21777
|
-
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
21778
|
-
*
|
|
21779
|
-
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
21780
|
-
*/
|
|
21781
|
-
var BobHooks = class BobHooks extends ToolHooks {
|
|
21782
|
-
constructor(params) {
|
|
21783
|
-
super({
|
|
21784
|
-
...params,
|
|
21785
|
-
fileContent: params.fileContent ?? "{}"
|
|
21786
|
-
});
|
|
21787
|
-
}
|
|
21788
|
-
isDeletable() {
|
|
21789
|
-
return false;
|
|
21790
|
-
}
|
|
21791
|
-
static getSettablePaths({ global = false } = {}) {
|
|
21792
|
-
return {
|
|
21793
|
-
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
21794
|
-
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
21795
|
-
};
|
|
21796
|
-
}
|
|
21797
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
21798
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21799
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
21800
|
-
return new BobHooks({
|
|
21801
|
-
outputRoot,
|
|
21802
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21803
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21804
|
-
fileContent,
|
|
21805
|
-
validate
|
|
21806
|
-
});
|
|
21807
|
-
}
|
|
21808
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
21809
|
-
const paths = BobHooks.getSettablePaths({ global });
|
|
21810
|
-
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21811
|
-
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
21812
|
-
const config = rulesyncHooks.getJson();
|
|
21813
|
-
const hooks = canonicalToToolHooks({
|
|
21814
|
-
config,
|
|
21815
|
-
toolOverrideHooks: config.bob?.hooks,
|
|
21816
|
-
converterConfig: BOB_CONVERTER_CONFIG,
|
|
21817
|
-
logger
|
|
21818
|
-
});
|
|
21819
|
-
const fileContent = applySharedConfigPatch({
|
|
21820
|
-
fileKey: sharedConfigFileKey(paths),
|
|
21821
|
-
feature: "hooks",
|
|
21822
|
-
existingContent,
|
|
21823
|
-
patch: { hooks },
|
|
21824
|
-
filePath,
|
|
21825
|
-
logger
|
|
21826
|
-
});
|
|
21827
|
-
return new BobHooks({
|
|
21828
|
-
outputRoot,
|
|
21829
|
-
relativeDirPath: paths.relativeDirPath,
|
|
21830
|
-
relativeFilePath: paths.relativeFilePath,
|
|
21831
|
-
fileContent,
|
|
21832
|
-
validate
|
|
21833
|
-
});
|
|
21834
|
-
}
|
|
21835
|
-
toRulesyncHooks({ logger } = {}) {
|
|
21836
|
-
const configPath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
21837
|
-
let settings;
|
|
21838
|
-
try {
|
|
21839
|
-
settings = parseBobSettings(this.getFileContent(), configPath);
|
|
21840
|
-
} catch (error) {
|
|
21841
|
-
throw new Error(`Failed to parse Bob hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
21842
|
-
}
|
|
21843
|
-
const hooks = toolHooksToCanonical({
|
|
21844
|
-
logger,
|
|
21845
|
-
hooks: settings.hooks,
|
|
21846
|
-
converterConfig: BOB_CONVERTER_CONFIG
|
|
21847
|
-
});
|
|
21848
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
21849
|
-
hooks,
|
|
21850
|
-
overrideKey: "bob"
|
|
21851
|
-
}), null, 2) });
|
|
21852
|
-
}
|
|
21853
|
-
validate() {
|
|
21854
|
-
return {
|
|
21855
|
-
success: true,
|
|
21856
|
-
error: null
|
|
21857
|
-
};
|
|
21858
|
-
}
|
|
21859
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
21860
|
-
return new BobHooks({
|
|
21861
|
-
outputRoot,
|
|
21862
|
-
relativeDirPath,
|
|
21863
|
-
relativeFilePath,
|
|
21864
|
-
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
21865
|
-
validate: false
|
|
21866
|
-
});
|
|
21867
|
-
}
|
|
21868
|
-
};
|
|
21869
|
-
//#endregion
|
|
21870
21841
|
//#region src/features/hooks/preserve-unowned-hook-commands.ts
|
|
21871
21842
|
/**
|
|
21872
21843
|
* Read the `hooks` value from a destination JSON file.
|
|
@@ -22153,75 +22124,22 @@ function warnSkip({ logger, event, expected }) {
|
|
|
22153
22124
|
logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
|
|
22154
22125
|
}
|
|
22155
22126
|
//#endregion
|
|
22156
|
-
//#region src/features/hooks/
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
|
|
22160
|
-
|
|
22161
|
-
|
|
22162
|
-
|
|
22163
|
-
|
|
22164
|
-
|
|
22165
|
-
|
|
22166
|
-
|
|
22167
|
-
|
|
22168
|
-
|
|
22169
|
-
|
|
22170
|
-
|
|
22171
|
-
|
|
22172
|
-
"beforeSubmitPrompt",
|
|
22173
|
-
"stop"
|
|
22174
|
-
]),
|
|
22175
|
-
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22176
|
-
"command",
|
|
22177
|
-
"prompt",
|
|
22178
|
-
"http",
|
|
22179
|
-
"mcp_tool",
|
|
22180
|
-
"agent"
|
|
22181
|
-
]),
|
|
22182
|
-
emitsPromptModel: true,
|
|
22183
|
-
stringPassthroughFields: [
|
|
22184
|
-
{
|
|
22185
|
-
canonical: "if",
|
|
22186
|
-
tool: "if"
|
|
22187
|
-
},
|
|
22188
|
-
{
|
|
22189
|
-
canonical: "statusMessage",
|
|
22190
|
-
tool: "statusMessage"
|
|
22191
|
-
},
|
|
22192
|
-
{
|
|
22193
|
-
canonical: "shell",
|
|
22194
|
-
tool: "shell",
|
|
22195
|
-
commandOnly: true
|
|
22196
|
-
}
|
|
22197
|
-
],
|
|
22198
|
-
booleanPassthroughFields: [
|
|
22199
|
-
{
|
|
22200
|
-
canonical: "once",
|
|
22201
|
-
tool: "once"
|
|
22202
|
-
},
|
|
22203
|
-
{
|
|
22204
|
-
canonical: "async",
|
|
22205
|
-
tool: "async",
|
|
22206
|
-
commandOnly: true
|
|
22207
|
-
},
|
|
22208
|
-
{
|
|
22209
|
-
canonical: "asyncRewake",
|
|
22210
|
-
tool: "asyncRewake",
|
|
22211
|
-
commandOnly: true
|
|
22212
|
-
},
|
|
22213
|
-
{
|
|
22214
|
-
canonical: "continueOnBlock",
|
|
22215
|
-
tool: "continueOnBlock"
|
|
22216
|
-
}
|
|
22217
|
-
],
|
|
22218
|
-
arrayPassthroughFields: [{
|
|
22219
|
-
canonical: "args",
|
|
22220
|
-
tool: "args",
|
|
22221
|
-
commandOnly: true
|
|
22222
|
-
}]
|
|
22223
|
-
};
|
|
22224
|
-
var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
22127
|
+
//#region src/features/hooks/settings-json-hooks.ts
|
|
22128
|
+
/**
|
|
22129
|
+
* Shared implementation for the tools whose hooks live under the top-level
|
|
22130
|
+
* `hooks` key of a JSON settings file in Claude Code's shape —
|
|
22131
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command", ... }] }] }`
|
|
22132
|
+
* — and whose file also holds settings rulesync does not own, so generation
|
|
22133
|
+
* merges the `hooks` key into it (see `SHARED_CONFIG_OWNERSHIP`) instead of
|
|
22134
|
+
* overwriting it, and `--delete` never removes the file wholesale.
|
|
22135
|
+
*
|
|
22136
|
+
* A concrete adapter supplies its {@link SettingsJsonHooksSpec} and its
|
|
22137
|
+
* settable paths; the event mapping, the matcher rules and the per-hook
|
|
22138
|
+
* fields the tool documents are all expressed in the spec's converter config.
|
|
22139
|
+
* An adapter that also supports preserving unowned hooks (Claude Code) opts in
|
|
22140
|
+
* through {@link ToolHooks.supportsPreserveUnowned}.
|
|
22141
|
+
*/
|
|
22142
|
+
var SettingsJsonHooks = class extends ToolHooks {
|
|
22225
22143
|
constructor(params) {
|
|
22226
22144
|
super({
|
|
22227
22145
|
...params,
|
|
@@ -22231,24 +22149,42 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22231
22149
|
isDeletable() {
|
|
22232
22150
|
return false;
|
|
22233
22151
|
}
|
|
22152
|
+
/** The tool-specific half of the adapter. Every concrete class overrides this. */
|
|
22153
|
+
static getSpec() {
|
|
22154
|
+
throw new Error(`${this.name} does not define getSpec()`);
|
|
22155
|
+
}
|
|
22234
22156
|
/**
|
|
22235
|
-
* The converter config used for both directions.
|
|
22236
|
-
*
|
|
22237
|
-
*
|
|
22157
|
+
* The converter config used for both directions. A separate hook so a
|
|
22158
|
+
* subclass can swap tool-specific details (e.g. the project directory
|
|
22159
|
+
* variable) without redefining the whole spec.
|
|
22238
22160
|
*/
|
|
22239
22161
|
static getConverterConfig() {
|
|
22240
|
-
return
|
|
22162
|
+
return this.getSpec().converterConfig;
|
|
22241
22163
|
}
|
|
22242
22164
|
static getSettablePaths(_options = {}) {
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22165
|
+
throw new Error(`${this.name} does not define getSettablePaths()`);
|
|
22166
|
+
}
|
|
22167
|
+
/**
|
|
22168
|
+
* The `SHARED_CONFIG_OWNERSHIP` key the settings file is merged under. By
|
|
22169
|
+
* default it is derived from the settable paths; an adapter whose file is
|
|
22170
|
+
* declared under another tool's key (the Claude Code plugin bundle) overrides
|
|
22171
|
+
* it.
|
|
22172
|
+
*/
|
|
22173
|
+
static getSharedFileKey(paths) {
|
|
22174
|
+
return sharedConfigFileKey(paths);
|
|
22175
|
+
}
|
|
22176
|
+
/**
|
|
22177
|
+
* `new this(params)` for the concrete adapter the static method was called
|
|
22178
|
+
* on; the cast is what an abstract class needs to be constructed through
|
|
22179
|
+
* `this`, and lives in one place.
|
|
22180
|
+
*/
|
|
22181
|
+
static instantiate(params) {
|
|
22182
|
+
return new this(params);
|
|
22247
22183
|
}
|
|
22248
22184
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22249
22185
|
const paths = this.getSettablePaths({ global });
|
|
22250
22186
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22251
|
-
return
|
|
22187
|
+
return this.instantiate({
|
|
22252
22188
|
outputRoot,
|
|
22253
22189
|
relativeDirPath: paths.relativeDirPath,
|
|
22254
22190
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22256,40 +22192,41 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22256
22192
|
validate
|
|
22257
22193
|
});
|
|
22258
22194
|
}
|
|
22259
|
-
static supportsPreserveUnowned() {
|
|
22260
|
-
return true;
|
|
22261
|
-
}
|
|
22262
22195
|
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
22263
|
-
return toolHooks instanceof
|
|
22196
|
+
return toolHooks instanceof this ? toolHooks.getOwnershipLockFiles() : [];
|
|
22264
22197
|
}
|
|
22265
22198
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
22266
22199
|
const paths = this.getSettablePaths({ global });
|
|
22267
22200
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22268
22201
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22269
22202
|
const config = rulesyncHooks.getJson();
|
|
22270
|
-
const
|
|
22203
|
+
const generatedHooks = canonicalToToolHooks({
|
|
22271
22204
|
config,
|
|
22272
|
-
toolOverrideHooks:
|
|
22205
|
+
toolOverrideHooks: overrideHooksOf({
|
|
22206
|
+
config,
|
|
22207
|
+
overrideKey: this.getSpec().overrideKey
|
|
22208
|
+
}),
|
|
22273
22209
|
converterConfig: this.getConverterConfig(),
|
|
22274
22210
|
logger
|
|
22275
22211
|
});
|
|
22276
22212
|
const preserving = preserveUnowned && this.supportsPreserveUnowned();
|
|
22277
22213
|
const merged = mergeGeneratedHookLists({
|
|
22278
22214
|
existingContent,
|
|
22279
|
-
generatedHooks
|
|
22215
|
+
generatedHooks,
|
|
22280
22216
|
shape: "matcher-groups",
|
|
22281
22217
|
preserveUnowned: preserving,
|
|
22282
22218
|
previouslyOwned: preserving ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
22283
22219
|
logger
|
|
22284
22220
|
});
|
|
22285
22221
|
const fileContent = applySharedConfigPatch({
|
|
22286
|
-
fileKey:
|
|
22222
|
+
fileKey: this.getSharedFileKey(paths),
|
|
22287
22223
|
feature: "hooks",
|
|
22288
22224
|
existingContent,
|
|
22289
22225
|
patch: { hooks: merged.hooks },
|
|
22290
|
-
filePath
|
|
22226
|
+
filePath,
|
|
22227
|
+
logger
|
|
22291
22228
|
});
|
|
22292
|
-
return
|
|
22229
|
+
return this.instantiate({
|
|
22293
22230
|
outputRoot,
|
|
22294
22231
|
relativeDirPath: paths.relativeDirPath,
|
|
22295
22232
|
relativeFilePath: paths.relativeFilePath,
|
|
@@ -22299,20 +22236,29 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22299
22236
|
});
|
|
22300
22237
|
}
|
|
22301
22238
|
toRulesyncHooks({ logger } = {}) {
|
|
22239
|
+
const ctor = this.constructor;
|
|
22240
|
+
const spec = ctor.getSpec();
|
|
22241
|
+
const configPath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
22302
22242
|
let settings;
|
|
22303
22243
|
try {
|
|
22304
|
-
settings =
|
|
22244
|
+
settings = parseSharedConfig({
|
|
22245
|
+
format: "json",
|
|
22246
|
+
fileContent: this.getFileContent(),
|
|
22247
|
+
filePath: configPath,
|
|
22248
|
+
invalidRootPolicy: "error"
|
|
22249
|
+
});
|
|
22305
22250
|
} catch (error) {
|
|
22306
|
-
|
|
22251
|
+
const reason = error instanceof Error && error.cause instanceof Error ? error.cause : error;
|
|
22252
|
+
throw new Error(`Failed to parse ${spec.displayName} hooks content in ${configPath}: ${formatError(reason)}`, { cause: error });
|
|
22307
22253
|
}
|
|
22308
22254
|
const hooks = toolHooksToCanonical({
|
|
22255
|
+
logger,
|
|
22309
22256
|
hooks: settings.hooks,
|
|
22310
|
-
converterConfig:
|
|
22311
|
-
logger
|
|
22257
|
+
converterConfig: ctor.getConverterConfig()
|
|
22312
22258
|
});
|
|
22313
22259
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
22314
22260
|
hooks,
|
|
22315
|
-
overrideKey:
|
|
22261
|
+
overrideKey: spec.overrideKey
|
|
22316
22262
|
}), null, 2) });
|
|
22317
22263
|
}
|
|
22318
22264
|
validate() {
|
|
@@ -22322,7 +22268,7 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22322
22268
|
};
|
|
22323
22269
|
}
|
|
22324
22270
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
22325
|
-
return
|
|
22271
|
+
return this.instantiate({
|
|
22326
22272
|
outputRoot,
|
|
22327
22273
|
relativeDirPath,
|
|
22328
22274
|
relativeFilePath,
|
|
@@ -22331,6 +22277,158 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
22331
22277
|
});
|
|
22332
22278
|
}
|
|
22333
22279
|
};
|
|
22280
|
+
/**
|
|
22281
|
+
* The `hooks` map of a tool's override block (`config.<overrideKey>.hooks`),
|
|
22282
|
+
* or `undefined` when the block states none. The schema keeps every override
|
|
22283
|
+
* block loose, so the lookup goes through the record form.
|
|
22284
|
+
*/
|
|
22285
|
+
function overrideHooksOf({ config, overrideKey }) {
|
|
22286
|
+
const override = lookupOwn({
|
|
22287
|
+
record: config,
|
|
22288
|
+
key: overrideKey
|
|
22289
|
+
});
|
|
22290
|
+
if (!isPlainObject$1(override)) return;
|
|
22291
|
+
const hooks = override.hooks;
|
|
22292
|
+
return isPlainObject$1(hooks) ? hooks : void 0;
|
|
22293
|
+
}
|
|
22294
|
+
//#endregion
|
|
22295
|
+
//#region src/features/hooks/bob-hooks.ts
|
|
22296
|
+
const BOB_SPEC = {
|
|
22297
|
+
displayName: "Bob",
|
|
22298
|
+
overrideKey: "bob",
|
|
22299
|
+
converterConfig: {
|
|
22300
|
+
supportedEvents: BOB_HOOK_EVENTS,
|
|
22301
|
+
canonicalToToolEventNames: CANONICAL_TO_BOB_EVENT_NAMES,
|
|
22302
|
+
toolToCanonicalEventNames: BOB_TO_CANONICAL_EVENT_NAMES,
|
|
22303
|
+
projectDirVar: "",
|
|
22304
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22305
|
+
"sessionStart",
|
|
22306
|
+
"beforeSubmitPrompt",
|
|
22307
|
+
"stop"
|
|
22308
|
+
]),
|
|
22309
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
22310
|
+
wildcardMatcherMeansAll: true
|
|
22311
|
+
}
|
|
22312
|
+
};
|
|
22313
|
+
/**
|
|
22314
|
+
* IBM Bob lifecycle hooks.
|
|
22315
|
+
*
|
|
22316
|
+
* Hooks live under the top-level `hooks` key of Bob's settings file —
|
|
22317
|
+
* `<project>/.bob/settings.json` (project scope) and
|
|
22318
|
+
* `~/.bob/settings/settings.json` (user scope) — in the Claude-Code shape:
|
|
22319
|
+
* `{ "<Event>": [{ "matcher"?: "<regex>", "hooks": [{ "type": "command",
|
|
22320
|
+
* "command": "...", "timeout"?: <seconds> }] }] }`. The file also holds
|
|
22321
|
+
* settings rulesync does not own, so generation merges the `hooks` key into it
|
|
22322
|
+
* (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting the file.
|
|
22323
|
+
*
|
|
22324
|
+
* @see https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
|
|
22325
|
+
*/
|
|
22326
|
+
var BobHooks = class extends SettingsJsonHooks {
|
|
22327
|
+
static getSpec() {
|
|
22328
|
+
return BOB_SPEC;
|
|
22329
|
+
}
|
|
22330
|
+
static getSettablePaths({ global = false } = {}) {
|
|
22331
|
+
return {
|
|
22332
|
+
relativeDirPath: global ? BOB_GLOBAL_SETTINGS_DIR_PATH : BOB_DIR,
|
|
22333
|
+
relativeFilePath: BOB_SETTINGS_FILE_NAME
|
|
22334
|
+
};
|
|
22335
|
+
}
|
|
22336
|
+
};
|
|
22337
|
+
//#endregion
|
|
22338
|
+
//#region src/features/hooks/claudecode-hooks.ts
|
|
22339
|
+
const CLAUDE_SPEC = {
|
|
22340
|
+
displayName: "Claude",
|
|
22341
|
+
overrideKey: "claudecode",
|
|
22342
|
+
converterConfig: {
|
|
22343
|
+
supportedEvents: CLAUDE_HOOK_EVENTS,
|
|
22344
|
+
canonicalToToolEventNames: CANONICAL_TO_CLAUDE_EVENT_NAMES,
|
|
22345
|
+
toolToCanonicalEventNames: CLAUDE_TO_CANONICAL_EVENT_NAMES,
|
|
22346
|
+
projectDirVar: "$CLAUDE_PROJECT_DIR",
|
|
22347
|
+
prefixDotRelativeCommandsOnly: true,
|
|
22348
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22349
|
+
"worktreeCreate",
|
|
22350
|
+
"worktreeRemove",
|
|
22351
|
+
"messageDisplay",
|
|
22352
|
+
"postToolBatch",
|
|
22353
|
+
"taskCreated",
|
|
22354
|
+
"taskCompleted",
|
|
22355
|
+
"teammateIdle",
|
|
22356
|
+
"cwdChanged",
|
|
22357
|
+
"beforeSubmitPrompt",
|
|
22358
|
+
"stop"
|
|
22359
|
+
]),
|
|
22360
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22361
|
+
"command",
|
|
22362
|
+
"prompt",
|
|
22363
|
+
"http",
|
|
22364
|
+
"mcp_tool",
|
|
22365
|
+
"agent"
|
|
22366
|
+
]),
|
|
22367
|
+
emitsPromptModel: true,
|
|
22368
|
+
stringPassthroughFields: [
|
|
22369
|
+
{
|
|
22370
|
+
canonical: "if",
|
|
22371
|
+
tool: "if"
|
|
22372
|
+
},
|
|
22373
|
+
{
|
|
22374
|
+
canonical: "statusMessage",
|
|
22375
|
+
tool: "statusMessage"
|
|
22376
|
+
},
|
|
22377
|
+
{
|
|
22378
|
+
canonical: "shell",
|
|
22379
|
+
tool: "shell",
|
|
22380
|
+
commandOnly: true
|
|
22381
|
+
}
|
|
22382
|
+
],
|
|
22383
|
+
booleanPassthroughFields: [
|
|
22384
|
+
{
|
|
22385
|
+
canonical: "once",
|
|
22386
|
+
tool: "once"
|
|
22387
|
+
},
|
|
22388
|
+
{
|
|
22389
|
+
canonical: "async",
|
|
22390
|
+
tool: "async",
|
|
22391
|
+
commandOnly: true
|
|
22392
|
+
},
|
|
22393
|
+
{
|
|
22394
|
+
canonical: "asyncRewake",
|
|
22395
|
+
tool: "asyncRewake",
|
|
22396
|
+
commandOnly: true
|
|
22397
|
+
},
|
|
22398
|
+
{
|
|
22399
|
+
canonical: "continueOnBlock",
|
|
22400
|
+
tool: "continueOnBlock"
|
|
22401
|
+
}
|
|
22402
|
+
],
|
|
22403
|
+
arrayPassthroughFields: [{
|
|
22404
|
+
canonical: "args",
|
|
22405
|
+
tool: "args",
|
|
22406
|
+
commandOnly: true
|
|
22407
|
+
}]
|
|
22408
|
+
}
|
|
22409
|
+
};
|
|
22410
|
+
var ClaudecodeHooks = class extends SettingsJsonHooks {
|
|
22411
|
+
static getSpec() {
|
|
22412
|
+
return CLAUDE_SPEC;
|
|
22413
|
+
}
|
|
22414
|
+
static getSettablePaths(_options = {}) {
|
|
22415
|
+
return {
|
|
22416
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
22417
|
+
relativeFilePath: CLAUDECODE_SETTINGS_FILE_NAME
|
|
22418
|
+
};
|
|
22419
|
+
}
|
|
22420
|
+
/**
|
|
22421
|
+
* Every Claude-shaped settings file rulesync writes for Claude Code — the
|
|
22422
|
+
* project and user `settings.json` and the plugin bundle's `hooks.json` — is
|
|
22423
|
+
* declared under the one Claude key in `SHARED_CONFIG_OWNERSHIP`.
|
|
22424
|
+
*/
|
|
22425
|
+
static getSharedFileKey(_paths) {
|
|
22426
|
+
return CLAUDE_SETTINGS_SHARED_FILE_KEY;
|
|
22427
|
+
}
|
|
22428
|
+
static supportsPreserveUnowned() {
|
|
22429
|
+
return true;
|
|
22430
|
+
}
|
|
22431
|
+
};
|
|
22334
22432
|
//#endregion
|
|
22335
22433
|
//#region src/features/hooks/claudecode-plugin-hooks.ts
|
|
22336
22434
|
var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
@@ -22858,54 +22956,45 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
22858
22956
|
};
|
|
22859
22957
|
//#endregion
|
|
22860
22958
|
//#region src/features/hooks/continue-hooks.ts
|
|
22861
|
-
const
|
|
22862
|
-
|
|
22863
|
-
|
|
22864
|
-
|
|
22865
|
-
|
|
22866
|
-
|
|
22867
|
-
|
|
22868
|
-
"
|
|
22869
|
-
|
|
22870
|
-
|
|
22871
|
-
|
|
22872
|
-
|
|
22873
|
-
|
|
22874
|
-
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
|
|
22878
|
-
|
|
22879
|
-
|
|
22880
|
-
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
|
|
22959
|
+
const CONTINUE_SPEC = {
|
|
22960
|
+
displayName: "Continue",
|
|
22961
|
+
overrideKey: "continue",
|
|
22962
|
+
converterConfig: {
|
|
22963
|
+
supportedEvents: CONTINUE_HOOK_EVENTS,
|
|
22964
|
+
canonicalToToolEventNames: CANONICAL_TO_CONTINUE_EVENT_NAMES,
|
|
22965
|
+
toolToCanonicalEventNames: CONTINUE_TO_CANONICAL_EVENT_NAMES,
|
|
22966
|
+
projectDirVar: "$CONTINUE_PROJECT_DIR",
|
|
22967
|
+
prefixDotRelativeCommandsOnly: true,
|
|
22968
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
22969
|
+
"beforeSubmitPrompt",
|
|
22970
|
+
"stop",
|
|
22971
|
+
"teammateIdle",
|
|
22972
|
+
"taskCompleted",
|
|
22973
|
+
"worktreeCreate",
|
|
22974
|
+
"worktreeRemove"
|
|
22975
|
+
]),
|
|
22976
|
+
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
22977
|
+
"command",
|
|
22978
|
+
"prompt",
|
|
22979
|
+
"http",
|
|
22980
|
+
"agent"
|
|
22981
|
+
]),
|
|
22982
|
+
emitsPromptModel: true,
|
|
22983
|
+
stringPassthroughFields: [{
|
|
22984
|
+
canonical: "statusMessage",
|
|
22985
|
+
tool: "statusMessage"
|
|
22986
|
+
}],
|
|
22987
|
+
booleanPassthroughFields: [{
|
|
22988
|
+
canonical: "once",
|
|
22989
|
+
tool: "once"
|
|
22990
|
+
}, {
|
|
22991
|
+
canonical: "async",
|
|
22992
|
+
tool: "async",
|
|
22993
|
+
commandOnly: true
|
|
22994
|
+
}]
|
|
22995
|
+
}
|
|
22894
22996
|
};
|
|
22895
22997
|
/**
|
|
22896
|
-
* Single spelling of the settings/hooks codec/policy: fail closed on an
|
|
22897
|
-
* unparseable root rather than replacing the user's Continue settings with
|
|
22898
|
-
* generated output.
|
|
22899
|
-
*/
|
|
22900
|
-
function parseContinueSettings(fileContent, filePath) {
|
|
22901
|
-
return parseSharedConfig({
|
|
22902
|
-
format: "json",
|
|
22903
|
-
fileContent,
|
|
22904
|
-
filePath,
|
|
22905
|
-
invalidRootPolicy: "error"
|
|
22906
|
-
});
|
|
22907
|
-
}
|
|
22908
|
-
/**
|
|
22909
22998
|
* Continue CLI hooks.
|
|
22910
22999
|
*
|
|
22911
23000
|
* Hooks live under the top-level `hooks` key of `<project>/.continue/settings.json`
|
|
@@ -22921,15 +23010,9 @@ function parseContinueSettings(fileContent, filePath) {
|
|
|
22921
23010
|
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/hookConfig.ts
|
|
22922
23011
|
* @see https://github.com/continuedev/continue/blob/main/extensions/cli/src/hooks/types.ts
|
|
22923
23012
|
*/
|
|
22924
|
-
var ContinueHooks = class
|
|
22925
|
-
|
|
22926
|
-
|
|
22927
|
-
...params,
|
|
22928
|
-
fileContent: params.fileContent ?? "{}"
|
|
22929
|
-
});
|
|
22930
|
-
}
|
|
22931
|
-
isDeletable() {
|
|
22932
|
-
return false;
|
|
23013
|
+
var ContinueHooks = class extends SettingsJsonHooks {
|
|
23014
|
+
static getSpec() {
|
|
23015
|
+
return CONTINUE_SPEC;
|
|
22933
23016
|
}
|
|
22934
23017
|
static getSettablePaths(_options = {}) {
|
|
22935
23018
|
return {
|
|
@@ -22937,77 +23020,6 @@ var ContinueHooks = class ContinueHooks extends ToolHooks {
|
|
|
22937
23020
|
relativeFilePath: CONTINUE_SETTINGS_FILE_NAME
|
|
22938
23021
|
};
|
|
22939
23022
|
}
|
|
22940
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
22941
|
-
const paths = ContinueHooks.getSettablePaths({ global });
|
|
22942
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
22943
|
-
return new ContinueHooks({
|
|
22944
|
-
outputRoot,
|
|
22945
|
-
relativeDirPath: paths.relativeDirPath,
|
|
22946
|
-
relativeFilePath: paths.relativeFilePath,
|
|
22947
|
-
fileContent,
|
|
22948
|
-
validate
|
|
22949
|
-
});
|
|
22950
|
-
}
|
|
22951
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
22952
|
-
const paths = ContinueHooks.getSettablePaths({ global });
|
|
22953
|
-
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22954
|
-
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
22955
|
-
const config = rulesyncHooks.getJson();
|
|
22956
|
-
const hooks = canonicalToToolHooks({
|
|
22957
|
-
config,
|
|
22958
|
-
toolOverrideHooks: config.continue?.hooks,
|
|
22959
|
-
converterConfig: CONTINUE_CONVERTER_CONFIG,
|
|
22960
|
-
logger
|
|
22961
|
-
});
|
|
22962
|
-
const fileContent = applySharedConfigPatch({
|
|
22963
|
-
fileKey: sharedConfigFileKey(paths),
|
|
22964
|
-
feature: "hooks",
|
|
22965
|
-
existingContent,
|
|
22966
|
-
patch: { hooks },
|
|
22967
|
-
filePath,
|
|
22968
|
-
logger
|
|
22969
|
-
});
|
|
22970
|
-
return new ContinueHooks({
|
|
22971
|
-
outputRoot,
|
|
22972
|
-
relativeDirPath: paths.relativeDirPath,
|
|
22973
|
-
relativeFilePath: paths.relativeFilePath,
|
|
22974
|
-
fileContent,
|
|
22975
|
-
validate
|
|
22976
|
-
});
|
|
22977
|
-
}
|
|
22978
|
-
toRulesyncHooks({ logger } = {}) {
|
|
22979
|
-
const configPath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
22980
|
-
let settings;
|
|
22981
|
-
try {
|
|
22982
|
-
settings = parseContinueSettings(this.getFileContent(), configPath);
|
|
22983
|
-
} catch (error) {
|
|
22984
|
-
throw new Error(`Failed to parse Continue hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
22985
|
-
}
|
|
22986
|
-
const hooks = toolHooksToCanonical({
|
|
22987
|
-
logger,
|
|
22988
|
-
hooks: settings.hooks,
|
|
22989
|
-
converterConfig: CONTINUE_CONVERTER_CONFIG
|
|
22990
|
-
});
|
|
22991
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
22992
|
-
hooks,
|
|
22993
|
-
overrideKey: "continue"
|
|
22994
|
-
}), null, 2) });
|
|
22995
|
-
}
|
|
22996
|
-
validate() {
|
|
22997
|
-
return {
|
|
22998
|
-
success: true,
|
|
22999
|
-
error: null
|
|
23000
|
-
};
|
|
23001
|
-
}
|
|
23002
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23003
|
-
return new ContinueHooks({
|
|
23004
|
-
outputRoot,
|
|
23005
|
-
relativeDirPath,
|
|
23006
|
-
relativeFilePath,
|
|
23007
|
-
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
23008
|
-
validate: false
|
|
23009
|
-
});
|
|
23010
|
-
}
|
|
23011
23023
|
};
|
|
23012
23024
|
//#endregion
|
|
23013
23025
|
//#region src/features/hooks/copilot-hooks.ts
|
|
@@ -23598,33 +23610,24 @@ const CORTEXCODE_SKILLS_DIR_PATH = join(CORTEXCODE_DIR, "skills");
|
|
|
23598
23610
|
const CORTEXCODE_GLOBAL_SKILLS_DIR_PATH = join(CORTEXCODE_GLOBAL_DIR_PATH, "skills");
|
|
23599
23611
|
//#endregion
|
|
23600
23612
|
//#region src/features/hooks/cortexcode-hooks.ts
|
|
23601
|
-
const
|
|
23602
|
-
|
|
23603
|
-
|
|
23604
|
-
|
|
23605
|
-
|
|
23606
|
-
|
|
23607
|
-
|
|
23608
|
-
|
|
23609
|
-
|
|
23610
|
-
|
|
23611
|
-
|
|
23612
|
-
|
|
23613
|
+
const CORTEXCODE_SPEC = {
|
|
23614
|
+
displayName: "Cortex Code",
|
|
23615
|
+
overrideKey: "cortexcode",
|
|
23616
|
+
converterConfig: {
|
|
23617
|
+
supportedEvents: CORTEXCODE_HOOK_EVENTS,
|
|
23618
|
+
canonicalToToolEventNames: CANONICAL_TO_CORTEXCODE_EVENT_NAMES,
|
|
23619
|
+
toolToCanonicalEventNames: CORTEXCODE_TO_CANONICAL_EVENT_NAMES,
|
|
23620
|
+
projectDirVar: "$CORTEX_PROJECT_DIR",
|
|
23621
|
+
prefixDotRelativeCommandsOnly: true,
|
|
23622
|
+
noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
|
|
23623
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
23624
|
+
booleanPassthroughFields: [{
|
|
23625
|
+
canonical: "enabled",
|
|
23626
|
+
tool: "enabled"
|
|
23627
|
+
}]
|
|
23628
|
+
}
|
|
23613
23629
|
};
|
|
23614
23630
|
/**
|
|
23615
|
-
* Single spelling of the settings/hooks codec/policy: fail closed on an
|
|
23616
|
-
* unparseable root rather than replacing the user's Cortex Code settings with
|
|
23617
|
-
* generated output.
|
|
23618
|
-
*/
|
|
23619
|
-
function parseCortexcodeSettings(fileContent, filePath) {
|
|
23620
|
-
return parseSharedConfig({
|
|
23621
|
-
format: "json",
|
|
23622
|
-
fileContent,
|
|
23623
|
-
filePath,
|
|
23624
|
-
invalidRootPolicy: "error"
|
|
23625
|
-
});
|
|
23626
|
-
}
|
|
23627
|
-
/**
|
|
23628
23631
|
* Snowflake Cortex Code hooks.
|
|
23629
23632
|
*
|
|
23630
23633
|
* Hooks live under the top-level `hooks` key of `<project>/.cortex/settings.json`
|
|
@@ -23633,20 +23636,16 @@ function parseCortexcodeSettings(fileContent, filePath) {
|
|
|
23633
23636
|
* "<regex>", "hooks": [{ "type": "command" | "prompt", ..., "timeout"?:
|
|
23634
23637
|
* <seconds>, "enabled"?: <boolean> }] }] }`. The project file also holds
|
|
23635
23638
|
* settings rulesync does not own, so generation merges the `hooks` key into
|
|
23636
|
-
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it
|
|
23639
|
+
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it, and
|
|
23640
|
+
* neither file is removed wholesale (hooks.json sits in the CLI-owned
|
|
23641
|
+
* `~/.snowflake/cortex/` tree).
|
|
23637
23642
|
*
|
|
23638
23643
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/extensibility
|
|
23639
23644
|
* @see https://docs.snowflake.com/en/user-guide/cortex-code/settings
|
|
23640
23645
|
*/
|
|
23641
|
-
var CortexcodeHooks = class
|
|
23642
|
-
|
|
23643
|
-
|
|
23644
|
-
...params,
|
|
23645
|
-
fileContent: params.fileContent ?? "{}"
|
|
23646
|
-
});
|
|
23647
|
-
}
|
|
23648
|
-
isDeletable() {
|
|
23649
|
-
return false;
|
|
23646
|
+
var CortexcodeHooks = class extends SettingsJsonHooks {
|
|
23647
|
+
static getSpec() {
|
|
23648
|
+
return CORTEXCODE_SPEC;
|
|
23650
23649
|
}
|
|
23651
23650
|
static getSettablePaths({ global = false } = {}) {
|
|
23652
23651
|
return global ? {
|
|
@@ -23657,60 +23656,338 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23657
23656
|
relativeFilePath: CORTEXCODE_SETTINGS_FILE_NAME
|
|
23658
23657
|
};
|
|
23659
23658
|
}
|
|
23659
|
+
};
|
|
23660
|
+
//#endregion
|
|
23661
|
+
//#region src/constants/crush-paths.ts
|
|
23662
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
23663
|
+
const CRUSH_GLOBAL_DIR = join(".config", "crush");
|
|
23664
|
+
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
23665
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
23666
|
+
const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
|
|
23667
|
+
const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
|
|
23668
|
+
const CRUSH_CONFIG_FILE_NAME = "crush.json";
|
|
23669
|
+
const CRUSH_HIDDEN_CONFIG_FILE_NAME = ".crush.json";
|
|
23670
|
+
const CRUSH_PERMISSIONS_KEY = "permissions";
|
|
23671
|
+
const CRUSH_ALLOWED_TOOLS_KEY = "allowed_tools";
|
|
23672
|
+
const CRUSH_OPTIONS_KEY = "options";
|
|
23673
|
+
const CRUSH_DISABLED_TOOLS_KEY = "disabled_tools";
|
|
23674
|
+
const CRUSH_HOOKS_KEY = "hooks";
|
|
23675
|
+
//#endregion
|
|
23676
|
+
//#region src/features/crush-config.ts
|
|
23677
|
+
/**
|
|
23678
|
+
* Where Crush's JSON config lives for a scope, before the project-scope twin
|
|
23679
|
+
* is resolved: `<project>/crush.json` or `~/.config/crush/crush.json`. This is
|
|
23680
|
+
* also the key every adapter reports through `getSettablePaths`, so the
|
|
23681
|
+
* shared-config ownership declaration, the gitignore derivation and the
|
|
23682
|
+
* shared-write ordering all see one file per scope.
|
|
23683
|
+
*/
|
|
23684
|
+
function getCrushConfigSettablePaths({ global = false } = {}) {
|
|
23685
|
+
return {
|
|
23686
|
+
relativeDirPath: global ? CRUSH_GLOBAL_DIR : ".",
|
|
23687
|
+
relativeFilePath: CRUSH_CONFIG_FILE_NAME
|
|
23688
|
+
};
|
|
23689
|
+
}
|
|
23690
|
+
/**
|
|
23691
|
+
* Resolves which Crush config file an adapter should read and write.
|
|
23692
|
+
*
|
|
23693
|
+
* At project scope Crush discovers both `.crush.json` and `crush.json` in the
|
|
23694
|
+
* working directory and merges every file it finds with `.crush.json` on top
|
|
23695
|
+
* (objects merge recursively, lists are concatenated, scalars are overridden).
|
|
23696
|
+
* Neither file therefore hides the other: an entry left in `crush.json` stays
|
|
23697
|
+
* in effect next to whatever `.crush.json` says. An existing `.crush.json` is
|
|
23698
|
+
* preferred because the user chose that spelling; otherwise `crush.json` is
|
|
23699
|
+
* used (and created when neither exists). The global scope has a single
|
|
23700
|
+
* spelling.
|
|
23701
|
+
*
|
|
23702
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
23703
|
+
*/
|
|
23704
|
+
async function resolveCrushConfigFile({ outputRoot, global = false }) {
|
|
23705
|
+
const paths = getCrushConfigSettablePaths({ global });
|
|
23706
|
+
const configDir = join(outputRoot, paths.relativeDirPath);
|
|
23707
|
+
const filePath = join(configDir, paths.relativeFilePath);
|
|
23708
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
23709
|
+
if (!global) {
|
|
23710
|
+
const hiddenPath = join(configDir, CRUSH_HIDDEN_CONFIG_FILE_NAME);
|
|
23711
|
+
const hiddenContent = await readFileContentOrNull(hiddenPath);
|
|
23712
|
+
if (hiddenContent !== null) return {
|
|
23713
|
+
relativeDirPath: paths.relativeDirPath,
|
|
23714
|
+
relativeFilePath: CRUSH_HIDDEN_CONFIG_FILE_NAME,
|
|
23715
|
+
filePath: hiddenPath,
|
|
23716
|
+
fileContent: hiddenContent,
|
|
23717
|
+
...fileContent === null ? {} : { twin: {
|
|
23718
|
+
filePath,
|
|
23719
|
+
fileContent
|
|
23720
|
+
} }
|
|
23721
|
+
};
|
|
23722
|
+
}
|
|
23723
|
+
return {
|
|
23724
|
+
relativeDirPath: paths.relativeDirPath,
|
|
23725
|
+
relativeFilePath: paths.relativeFilePath,
|
|
23726
|
+
filePath,
|
|
23727
|
+
fileContent
|
|
23728
|
+
};
|
|
23729
|
+
}
|
|
23730
|
+
/**
|
|
23731
|
+
* Single spelling of the crush.json codec/policy, matching the
|
|
23732
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
23733
|
+
* unparseable root rather than replacing the user's Crush config with
|
|
23734
|
+
* generated output.
|
|
23735
|
+
*/
|
|
23736
|
+
function parseCrushConfig(fileContent, filePath) {
|
|
23737
|
+
return parseSharedConfig({
|
|
23738
|
+
format: "json",
|
|
23739
|
+
fileContent,
|
|
23740
|
+
filePath,
|
|
23741
|
+
invalidRootPolicy: "error"
|
|
23742
|
+
});
|
|
23743
|
+
}
|
|
23744
|
+
/**
|
|
23745
|
+
* Combine two Crush config documents the way Crush itself does
|
|
23746
|
+
* (`github.com/qjebbs/go-jsons`): objects merge recursively, arrays are
|
|
23747
|
+
* concatenated with `base` first, and any other value from `override` wins.
|
|
23748
|
+
*/
|
|
23749
|
+
function mergeCrushConfigs({ base, override }) {
|
|
23750
|
+
const merged = { ...base };
|
|
23751
|
+
for (const [key, value] of Object.entries(override)) {
|
|
23752
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
23753
|
+
const current = merged[key];
|
|
23754
|
+
if (isRecord$1(current) && isRecord$1(value)) merged[key] = mergeCrushConfigs({
|
|
23755
|
+
base: current,
|
|
23756
|
+
override: value
|
|
23757
|
+
});
|
|
23758
|
+
else if (Array.isArray(current) && Array.isArray(value)) merged[key] = [...current, ...value];
|
|
23759
|
+
else merged[key] = value;
|
|
23760
|
+
}
|
|
23761
|
+
return merged;
|
|
23762
|
+
}
|
|
23763
|
+
/**
|
|
23764
|
+
* The content an import should read for a resolved location: the chosen file
|
|
23765
|
+
* merged over its twin when both exist, so a server, hook or tool entry that
|
|
23766
|
+
* only the lower-priority `crush.json` declares is imported too — Crush sees
|
|
23767
|
+
* it, so rulesync should as well.
|
|
23768
|
+
*/
|
|
23769
|
+
function crushConfigImportContent(location) {
|
|
23770
|
+
const own = location.fileContent ?? "{}";
|
|
23771
|
+
if (location.twin === void 0) return own;
|
|
23772
|
+
const merged = mergeCrushConfigs({
|
|
23773
|
+
base: parseCrushConfig(location.twin.fileContent, location.twin.filePath),
|
|
23774
|
+
override: parseCrushConfig(own, location.filePath)
|
|
23775
|
+
});
|
|
23776
|
+
return JSON.stringify(merged, null, 2);
|
|
23777
|
+
}
|
|
23778
|
+
function lookupPath(root, path) {
|
|
23779
|
+
let current = root;
|
|
23780
|
+
for (const segment of path) {
|
|
23781
|
+
if (!isRecord$1(current) || !Object.hasOwn(current, segment)) return;
|
|
23782
|
+
current = current[segment];
|
|
23783
|
+
}
|
|
23784
|
+
return current;
|
|
23785
|
+
}
|
|
23786
|
+
function isNonEmptyValue(value) {
|
|
23787
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
23788
|
+
if (isRecord$1(value)) return Object.keys(value).length > 0;
|
|
23789
|
+
return value !== void 0;
|
|
23790
|
+
}
|
|
23791
|
+
/**
|
|
23792
|
+
* Warn when the twin Crush merges beneath the written file still carries a
|
|
23793
|
+
* value at one of the paths this feature owns. Rulesync only rewrites the
|
|
23794
|
+
* chosen file, and because Crush concatenates lists and merges objects across
|
|
23795
|
+
* the pair, such an entry — typically one an earlier generate wrote to
|
|
23796
|
+
* `crush.json` before the user added a `.crush.json` — stays in effect until
|
|
23797
|
+
* removed by hand, and cannot be retracted from `.rulesync/`.
|
|
23798
|
+
*/
|
|
23799
|
+
function warnCrushTwinLeftovers({ location, ownedPaths, logger }) {
|
|
23800
|
+
if (location.twin === void 0) return;
|
|
23801
|
+
let twin;
|
|
23802
|
+
try {
|
|
23803
|
+
twin = parseCrushConfig(location.twin.fileContent, location.twin.filePath);
|
|
23804
|
+
} catch {
|
|
23805
|
+
return;
|
|
23806
|
+
}
|
|
23807
|
+
const leftovers = ownedPaths.filter((path) => isNonEmptyValue(lookupPath(twin, path))).map((path) => `"${path.join(".")}"`);
|
|
23808
|
+
if (leftovers.length === 0) return;
|
|
23809
|
+
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.`);
|
|
23810
|
+
}
|
|
23811
|
+
//#endregion
|
|
23812
|
+
//#region src/features/hooks/crush-hooks.ts
|
|
23813
|
+
const SUPPORTED_CRUSH_EVENTS = new Set(CRUSH_HOOK_EVENTS);
|
|
23814
|
+
/**
|
|
23815
|
+
* Crush matches an event key case-insensitively and ignores underscores
|
|
23816
|
+
* (`PreToolUse`, `pre_tool_use` and `PRE_TOOL_USE` all work), so an import
|
|
23817
|
+
* looks the canonical name up by that normalized spelling.
|
|
23818
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
23819
|
+
*/
|
|
23820
|
+
const NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CRUSH_TO_CANONICAL_EVENT_NAMES).map(([crushEvent, canonical]) => [normalizeCrushEventName(crushEvent), canonical]));
|
|
23821
|
+
function normalizeCrushEventName(event) {
|
|
23822
|
+
return event.replaceAll("_", "").toLowerCase();
|
|
23823
|
+
}
|
|
23824
|
+
/**
|
|
23825
|
+
* Build the `hooks` block of `crush.json` from a canonical hooks config.
|
|
23826
|
+
* Crush keys a flat array of `{name, matcher, command, timeout}` entries by
|
|
23827
|
+
* event name; `matcher` is a regex tested against the (lower-case) Crush tool
|
|
23828
|
+
* name and `timeout` is in seconds. Only `type: "command"` canonical hooks are
|
|
23829
|
+
* emitted, since a Crush hook is a shell command. A shared canonical event
|
|
23830
|
+
* Crush does not fire is skipped (the HooksProcessor reports it), while an
|
|
23831
|
+
* event under the `crush.hooks` override — such as one an import filed there
|
|
23832
|
+
* — is written verbatim so it round-trips.
|
|
23833
|
+
*/
|
|
23834
|
+
function canonicalToCrushHooks({ config, toolOverride, logger }) {
|
|
23835
|
+
const sharedHooks = {};
|
|
23836
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_CRUSH_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
23837
|
+
const effective = {
|
|
23838
|
+
...sharedHooks,
|
|
23839
|
+
...toolOverride
|
|
23840
|
+
};
|
|
23841
|
+
const hooks = {};
|
|
23842
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
23843
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
23844
|
+
const crushEvent = lookupOwn({
|
|
23845
|
+
record: CANONICAL_TO_CRUSH_EVENT_NAMES,
|
|
23846
|
+
key: event
|
|
23847
|
+
}) ?? event;
|
|
23848
|
+
const entries = [];
|
|
23849
|
+
for (const def of defs) {
|
|
23850
|
+
const entry = canonicalDefToCrushEntry({
|
|
23851
|
+
def,
|
|
23852
|
+
event,
|
|
23853
|
+
logger
|
|
23854
|
+
});
|
|
23855
|
+
if (entry !== null) entries.push(entry);
|
|
23856
|
+
}
|
|
23857
|
+
if (entries.length > 0) hooks[crushEvent] = entries;
|
|
23858
|
+
}
|
|
23859
|
+
return hooks;
|
|
23860
|
+
}
|
|
23861
|
+
/** Convert one canonical hook definition to a Crush entry, or null to skip. */
|
|
23862
|
+
function canonicalDefToCrushEntry({ def, event, logger }) {
|
|
23863
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
23864
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
23865
|
+
logger?.warn(`Crush hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Crush would discard, so it was skipped.`);
|
|
23866
|
+
return null;
|
|
23867
|
+
}
|
|
23868
|
+
const entry = { command: def.command };
|
|
23869
|
+
if (typeof def.name === "string" && def.name !== "") entry.name = def.name;
|
|
23870
|
+
if (typeof def.matcher === "string" && def.matcher !== "" && def.matcher !== "*") entry.matcher = def.matcher;
|
|
23871
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
23872
|
+
return entry;
|
|
23873
|
+
}
|
|
23874
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
23875
|
+
function crushEntryToCanonicalDef(raw) {
|
|
23876
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string") return null;
|
|
23877
|
+
const def = {
|
|
23878
|
+
type: "command",
|
|
23879
|
+
command: raw.command
|
|
23880
|
+
};
|
|
23881
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
23882
|
+
if (typeof raw.matcher === "string" && raw.matcher !== "") def.matcher = raw.matcher;
|
|
23883
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
23884
|
+
return def;
|
|
23885
|
+
}
|
|
23886
|
+
/**
|
|
23887
|
+
* Reverse {@link canonicalToCrushHooks}: parse the `hooks` block back into a
|
|
23888
|
+
* canonical event → definition[] record. An event Crush does not document is
|
|
23889
|
+
* carried under its own name so `buildImportedHooksConfig` files it under the
|
|
23890
|
+
* `crush.hooks` override.
|
|
23891
|
+
*/
|
|
23892
|
+
function crushHooksToCanonical(hooksBlock) {
|
|
23893
|
+
const canonical = {};
|
|
23894
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
23895
|
+
for (const [crushEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
23896
|
+
if (isPrototypePollutionKey(crushEvent) || !Array.isArray(rawEntries)) continue;
|
|
23897
|
+
const canonicalEvent = lookupOwn({
|
|
23898
|
+
record: NORMALIZED_CRUSH_TO_CANONICAL_EVENT_NAMES,
|
|
23899
|
+
key: normalizeCrushEventName(crushEvent)
|
|
23900
|
+
}) ?? crushEvent;
|
|
23901
|
+
const defs = rawEntries.map((raw) => crushEntryToCanonicalDef(raw)).filter((def) => def !== null);
|
|
23902
|
+
if (defs.length === 0) continue;
|
|
23903
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
23904
|
+
record: canonical,
|
|
23905
|
+
key: canonicalEvent
|
|
23906
|
+
}) ?? [], ...defs];
|
|
23907
|
+
}
|
|
23908
|
+
return canonical;
|
|
23909
|
+
}
|
|
23910
|
+
/**
|
|
23911
|
+
* Crush hooks.
|
|
23912
|
+
*
|
|
23913
|
+
* Crush reads hooks from the `hooks` key of its JSON config —
|
|
23914
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
23915
|
+
* lists concatenated) at project scope and `~/.config/crush/crush.json` at
|
|
23916
|
+
* user scope — as
|
|
23917
|
+
* `hooks.<Event>: [{name?, matcher?, command, timeout?}]`. Only `PreToolUse`
|
|
23918
|
+
* fires today. The `hooks` key is owned outright; every other top-level key
|
|
23919
|
+
* of the file is preserved and the file is never deleted.
|
|
23920
|
+
*
|
|
23921
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/hooks/README.md
|
|
23922
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
23923
|
+
*/
|
|
23924
|
+
var CrushHooks = class CrushHooks extends ToolHooks {
|
|
23925
|
+
json;
|
|
23926
|
+
constructor(params) {
|
|
23927
|
+
super(params);
|
|
23928
|
+
this.json = parseCrushConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
23929
|
+
}
|
|
23930
|
+
getJson() {
|
|
23931
|
+
return this.json;
|
|
23932
|
+
}
|
|
23933
|
+
isDeletable() {
|
|
23934
|
+
return false;
|
|
23935
|
+
}
|
|
23936
|
+
static getSettablePaths({ global = false } = {}) {
|
|
23937
|
+
return getCrushConfigSettablePaths({ global });
|
|
23938
|
+
}
|
|
23660
23939
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
23661
|
-
const
|
|
23662
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
23663
|
-
return new CortexcodeHooks({
|
|
23940
|
+
const location = await resolveCrushConfigFile({
|
|
23664
23941
|
outputRoot,
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
|
|
23942
|
+
global
|
|
23943
|
+
});
|
|
23944
|
+
return new CrushHooks({
|
|
23945
|
+
outputRoot,
|
|
23946
|
+
relativeDirPath: location.relativeDirPath,
|
|
23947
|
+
relativeFilePath: location.relativeFilePath,
|
|
23948
|
+
fileContent: crushConfigImportContent(location),
|
|
23949
|
+
validate,
|
|
23950
|
+
global
|
|
23669
23951
|
});
|
|
23670
23952
|
}
|
|
23671
23953
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
23672
|
-
const
|
|
23673
|
-
|
|
23674
|
-
|
|
23675
|
-
|
|
23676
|
-
const
|
|
23677
|
-
|
|
23678
|
-
|
|
23679
|
-
|
|
23954
|
+
const location = await resolveCrushConfigFile({
|
|
23955
|
+
outputRoot,
|
|
23956
|
+
global
|
|
23957
|
+
});
|
|
23958
|
+
const existingContent = location.fileContent ?? "";
|
|
23959
|
+
warnCrushTwinLeftovers({
|
|
23960
|
+
location,
|
|
23961
|
+
ownedPaths: [[CRUSH_HOOKS_KEY]],
|
|
23680
23962
|
logger
|
|
23681
23963
|
});
|
|
23682
|
-
const
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
patch: { hooks },
|
|
23687
|
-
filePath,
|
|
23964
|
+
const config = rulesyncHooks.getJson();
|
|
23965
|
+
const hooks = canonicalToCrushHooks({
|
|
23966
|
+
config,
|
|
23967
|
+
toolOverride: config.crush?.hooks,
|
|
23688
23968
|
logger
|
|
23689
23969
|
});
|
|
23690
|
-
return new
|
|
23970
|
+
return new CrushHooks({
|
|
23691
23971
|
outputRoot,
|
|
23692
|
-
relativeDirPath:
|
|
23693
|
-
relativeFilePath:
|
|
23694
|
-
fileContent
|
|
23695
|
-
|
|
23972
|
+
relativeDirPath: location.relativeDirPath,
|
|
23973
|
+
relativeFilePath: location.relativeFilePath,
|
|
23974
|
+
fileContent: applySharedConfigPatch({
|
|
23975
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
23976
|
+
feature: "hooks",
|
|
23977
|
+
existingContent,
|
|
23978
|
+
patch: { [CRUSH_HOOKS_KEY]: hooks },
|
|
23979
|
+
filePath: location.filePath,
|
|
23980
|
+
logger
|
|
23981
|
+
}),
|
|
23982
|
+
validate,
|
|
23983
|
+
global
|
|
23696
23984
|
});
|
|
23697
23985
|
}
|
|
23698
|
-
toRulesyncHooks(
|
|
23699
|
-
const
|
|
23700
|
-
let settings;
|
|
23701
|
-
try {
|
|
23702
|
-
settings = parseCortexcodeSettings(this.getFileContent(), configPath);
|
|
23703
|
-
} catch (error) {
|
|
23704
|
-
throw new Error(`Failed to parse Cortex Code hooks content in ${configPath}: ${formatError(error)}`, { cause: error });
|
|
23705
|
-
}
|
|
23706
|
-
const hooks = toolHooksToCanonical({
|
|
23707
|
-
logger,
|
|
23708
|
-
hooks: settings.hooks,
|
|
23709
|
-
converterConfig: CORTEXCODE_CONVERTER_CONFIG
|
|
23710
|
-
});
|
|
23986
|
+
toRulesyncHooks() {
|
|
23987
|
+
const hooks = crushHooksToCanonical(this.json[CRUSH_HOOKS_KEY]);
|
|
23711
23988
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
23712
23989
|
hooks,
|
|
23713
|
-
overrideKey: "
|
|
23990
|
+
overrideKey: "crush"
|
|
23714
23991
|
}), null, 2) });
|
|
23715
23992
|
}
|
|
23716
23993
|
validate() {
|
|
@@ -23719,13 +23996,14 @@ var CortexcodeHooks = class CortexcodeHooks extends ToolHooks {
|
|
|
23719
23996
|
error: null
|
|
23720
23997
|
};
|
|
23721
23998
|
}
|
|
23722
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
23723
|
-
return new
|
|
23999
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
24000
|
+
return new CrushHooks({
|
|
23724
24001
|
outputRoot,
|
|
23725
24002
|
relativeDirPath,
|
|
23726
24003
|
relativeFilePath,
|
|
23727
|
-
fileContent: JSON.stringify({
|
|
23728
|
-
validate: false
|
|
24004
|
+
fileContent: JSON.stringify({ [CRUSH_HOOKS_KEY]: {} }, null, 2),
|
|
24005
|
+
validate: false,
|
|
24006
|
+
global
|
|
23729
24007
|
});
|
|
23730
24008
|
}
|
|
23731
24009
|
};
|
|
@@ -26959,19 +27237,22 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
|
26959
27237
|
//#endregion
|
|
26960
27238
|
//#region src/features/hooks/tabnine-hooks.ts
|
|
26961
27239
|
/**
|
|
26962
|
-
* Environment block safe to hand Tabnine for a hook.
|
|
26963
|
-
*
|
|
26964
|
-
*
|
|
26965
|
-
*
|
|
26966
|
-
*
|
|
27240
|
+
* Environment block safe to hand Tabnine for a hook. The shared converter
|
|
27241
|
+
* refuses a whole `env` map holding one bad entry; Tabnine's shape is authored
|
|
27242
|
+
* outside it, so the same per-entry rule (`isSafeEnvEntry`: a key holding `=`,
|
|
27243
|
+
* a control character or nothing at all names a different variable than it
|
|
27244
|
+
* appears to) is applied entry by entry and the bad ones are dropped in both
|
|
27245
|
+
* directions, with a warning on export, where the value came from an authored
|
|
27246
|
+
* `.rulesync/hooks.*`.
|
|
26967
27247
|
*/
|
|
26968
27248
|
function sanitizeEnv({ env, warn }) {
|
|
26969
27249
|
if (env === null || env === void 0 || typeof env !== "object" || Array.isArray(env)) return;
|
|
26970
27250
|
const result = {};
|
|
26971
27251
|
for (const [key, value] of Object.entries(env)) {
|
|
26972
|
-
|
|
26973
|
-
|
|
26974
|
-
|
|
27252
|
+
if (!isSafeEnvEntry({
|
|
27253
|
+
key,
|
|
27254
|
+
value
|
|
27255
|
+
})) {
|
|
26975
27256
|
warn?.(`Tabnine CLI hook env entry ${JSON.stringify(key)} is not a safe KEY=VALUE pair; skipping it.`);
|
|
26976
27257
|
continue;
|
|
26977
27258
|
}
|
|
@@ -27072,7 +27353,7 @@ const TabnineMatcherEntrySchema = z.looseObject({
|
|
|
27072
27353
|
});
|
|
27073
27354
|
function tabnineMatcherEntryToCanonical(entry) {
|
|
27074
27355
|
const sequential = entry.sequential === true;
|
|
27075
|
-
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
27356
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && entry.matcher !== ".*" ? entry.matcher : void 0;
|
|
27076
27357
|
const defs = [];
|
|
27077
27358
|
for (const hook of entry.hooks ?? []) {
|
|
27078
27359
|
if (hook.type !== "command" || typeof hook.command !== "string" || hook.command === "") continue;
|
|
@@ -28114,6 +28395,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28114
28395
|
supportedHookTypes: ["command", "http"],
|
|
28115
28396
|
supportsMatcher: true
|
|
28116
28397
|
}],
|
|
28398
|
+
["crush", {
|
|
28399
|
+
class: CrushHooks,
|
|
28400
|
+
meta: {
|
|
28401
|
+
supportsProject: true,
|
|
28402
|
+
supportsGlobal: true,
|
|
28403
|
+
supportsImport: true
|
|
28404
|
+
},
|
|
28405
|
+
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
28406
|
+
supportedHookTypes: ["command"],
|
|
28407
|
+
supportsMatcher: true,
|
|
28408
|
+
passthroughOverrideEvents: true
|
|
28409
|
+
}],
|
|
28117
28410
|
["zcode", {
|
|
28118
28411
|
class: ZcodeHooks,
|
|
28119
28412
|
meta: {
|
|
@@ -28795,14 +29088,6 @@ var ContinueIgnore = class ContinueIgnore extends ToolIgnore {
|
|
|
28795
29088
|
}
|
|
28796
29089
|
};
|
|
28797
29090
|
//#endregion
|
|
28798
|
-
//#region src/constants/crush-paths.ts
|
|
28799
|
-
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
28800
|
-
const CRUSH_GLOBAL_DIR = join(".config", "crush");
|
|
28801
|
-
const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
|
|
28802
|
-
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
28803
|
-
const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
|
|
28804
|
-
const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
|
|
28805
|
-
//#endregion
|
|
28806
29091
|
//#region src/features/ignore/crush-ignore.ts
|
|
28807
29092
|
/**
|
|
28808
29093
|
* Ignore generator for Crush.
|
|
@@ -30804,9 +31089,9 @@ function asBobRemoteType(stated, url) {
|
|
|
30804
31089
|
* SSE server carries a bare `url`. The canonical `transport` alias and the
|
|
30805
31090
|
* Claude-style `httpUrl` alias are folded into `type`/`url`; `env`, `cwd`,
|
|
30806
31091
|
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through, as Bob
|
|
30807
|
-
* documents all of them
|
|
30808
|
-
*
|
|
30809
|
-
* the HTTP requests).
|
|
31092
|
+
* documents all of them, with prototype-pollution keys dropped at every
|
|
31093
|
+
* nesting level (Bob spreads `env` and `headers` into the process environment
|
|
31094
|
+
* and the HTTP requests).
|
|
30810
31095
|
*
|
|
30811
31096
|
* Bob Shell documents the same file with an `httpURL` key instead of
|
|
30812
31097
|
* `type` + `url` for streamable HTTP. rulesync writes the IDE spelling (the two
|
|
@@ -30874,7 +31159,7 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30874
31159
|
}
|
|
30875
31160
|
for (const [key, value] of Object.entries(rest)) {
|
|
30876
31161
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30877
|
-
converted[key] = (
|
|
31162
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30878
31163
|
}
|
|
30879
31164
|
result[serverName] = converted;
|
|
30880
31165
|
}
|
|
@@ -30885,7 +31170,9 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30885
31170
|
* (`type: "streamable-http"` + `url`) is already canonical and passes through;
|
|
30886
31171
|
* the Bob Shell spelling `httpURL` becomes `url` with `type: "http"`; a bare
|
|
30887
31172
|
* `url` is an SSE server in Bob, so it gains `type: "sse"` to keep that reading
|
|
30888
|
-
* on the next generate.
|
|
31173
|
+
* on the next generate. Every other field passes through with its
|
|
31174
|
+
* prototype-pollution keys dropped at every nesting level, mirroring the
|
|
31175
|
+
* generate side's handling of `env` / `headers`.
|
|
30889
31176
|
*/
|
|
30890
31177
|
function convertFromBobFormat(mcpServers) {
|
|
30891
31178
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -30900,7 +31187,7 @@ function convertFromBobFormat(mcpServers) {
|
|
|
30900
31187
|
if (typeof value === "string") httpURL = value;
|
|
30901
31188
|
continue;
|
|
30902
31189
|
}
|
|
30903
|
-
converted[key] = value;
|
|
31190
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30904
31191
|
}
|
|
30905
31192
|
if (httpURL !== void 0) {
|
|
30906
31193
|
converted.url = httpURL;
|
|
@@ -32306,7 +32593,7 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32306
32593
|
}
|
|
32307
32594
|
for (const [key, value] of Object.entries(rest)) {
|
|
32308
32595
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32309
|
-
converted[key] = (
|
|
32596
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
32310
32597
|
}
|
|
32311
32598
|
result[serverName] = converted;
|
|
32312
32599
|
}
|
|
@@ -32315,14 +32602,15 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32315
32602
|
/**
|
|
32316
32603
|
* Convert Cortex Code's server map back to the canonical shape. The
|
|
32317
32604
|
* documented spelling (`type` + `command`/`url`) is already canonical, so
|
|
32318
|
-
* entries pass through with only prototype-pollution keys dropped
|
|
32605
|
+
* entries pass through with only prototype-pollution keys dropped — at every
|
|
32606
|
+
* nesting level, mirroring the generate side.
|
|
32319
32607
|
*/
|
|
32320
32608
|
function convertFromCortexcodeFormat(mcpServers) {
|
|
32321
32609
|
if (!isMcpServers(mcpServers)) return {};
|
|
32322
32610
|
const result = {};
|
|
32323
32611
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32324
32612
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32325
|
-
result[serverName] =
|
|
32613
|
+
result[serverName] = omitPrototypePollutionKeysDeep(serverConfig);
|
|
32326
32614
|
}
|
|
32327
32615
|
return result;
|
|
32328
32616
|
}
|
|
@@ -32361,19 +32649,12 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32361
32649
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32362
32650
|
if (!global) throw new Error(CORTEXCODE_GLOBAL_ONLY_MESSAGE);
|
|
32363
32651
|
const paths = this.getSettablePaths({ global });
|
|
32364
|
-
const
|
|
32365
|
-
fileContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32366
|
-
relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
|
|
32367
|
-
});
|
|
32368
|
-
const newJson = {
|
|
32369
|
-
...json,
|
|
32370
|
-
mcpServers: json.mcpServers ?? {}
|
|
32371
|
-
};
|
|
32652
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32372
32653
|
return new CortexcodeMcp({
|
|
32373
32654
|
outputRoot,
|
|
32374
32655
|
relativeDirPath: paths.relativeDirPath,
|
|
32375
32656
|
relativeFilePath: paths.relativeFilePath,
|
|
32376
|
-
fileContent
|
|
32657
|
+
fileContent,
|
|
32377
32658
|
validate,
|
|
32378
32659
|
global
|
|
32379
32660
|
});
|
|
@@ -32421,6 +32702,319 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32421
32702
|
}
|
|
32422
32703
|
};
|
|
32423
32704
|
//#endregion
|
|
32705
|
+
//#region src/features/mcp/crush-mcp.ts
|
|
32706
|
+
/**
|
|
32707
|
+
* Crush-only MCP fields with no canonical counterpart, authored under a
|
|
32708
|
+
* `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
|
|
32709
|
+
* name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers and
|
|
32710
|
+
* `sessionless` marks a server that issues no `Mcp-Session-Id`.
|
|
32711
|
+
* `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
|
|
32712
|
+
* never reaches another tool's config; `fromRulesyncMcp` re-merges them from
|
|
32713
|
+
* the raw source JSON, honouring the raw Crush spelling as a fallback for an
|
|
32714
|
+
* entry copied straight out of a `crush.json` (the `experimental_environment`
|
|
32715
|
+
* precedent) — except `oauth`, whose canonical key is Claude Code's
|
|
32716
|
+
* `{ clientId }` object and which only `crushOauth` can set.
|
|
32717
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
32718
|
+
*/
|
|
32719
|
+
const CRUSH_ONLY_KEYS = [
|
|
32720
|
+
{
|
|
32721
|
+
canonical: "crushOauth",
|
|
32722
|
+
crush: "oauth",
|
|
32723
|
+
accepts: (v) => typeof v === "boolean",
|
|
32724
|
+
rawFallback: false
|
|
32725
|
+
},
|
|
32726
|
+
{
|
|
32727
|
+
canonical: "crushOauthClientId",
|
|
32728
|
+
crush: "oauth_client_id",
|
|
32729
|
+
accepts: (v) => typeof v === "string",
|
|
32730
|
+
rawFallback: true
|
|
32731
|
+
},
|
|
32732
|
+
{
|
|
32733
|
+
canonical: "crushOauthClientSecret",
|
|
32734
|
+
crush: "oauth_client_secret",
|
|
32735
|
+
accepts: (v) => typeof v === "string",
|
|
32736
|
+
rawFallback: true
|
|
32737
|
+
},
|
|
32738
|
+
{
|
|
32739
|
+
canonical: "crushOauthCallbackPort",
|
|
32740
|
+
crush: "oauth_callback_port",
|
|
32741
|
+
accepts: (v) => Number.isInteger(v),
|
|
32742
|
+
rawFallback: true
|
|
32743
|
+
},
|
|
32744
|
+
{
|
|
32745
|
+
canonical: "crushSessionless",
|
|
32746
|
+
crush: "sessionless",
|
|
32747
|
+
accepts: (v) => typeof v === "boolean",
|
|
32748
|
+
rawFallback: true
|
|
32749
|
+
}
|
|
32750
|
+
];
|
|
32751
|
+
/**
|
|
32752
|
+
* The Crush-only keys of one raw canonical server entry, keyed by their
|
|
32753
|
+
* canonical name, so `convertToCrushFormat` sees them after the shared map
|
|
32754
|
+
* stripped them.
|
|
32755
|
+
*/
|
|
32756
|
+
function readCrushOnlyKeys(rawServer) {
|
|
32757
|
+
const result = {};
|
|
32758
|
+
if (!isRecord$1(rawServer)) return result;
|
|
32759
|
+
for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
|
|
32760
|
+
const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
|
|
32761
|
+
if (value !== void 0) result[canonical] = value;
|
|
32762
|
+
}
|
|
32763
|
+
return result;
|
|
32764
|
+
}
|
|
32765
|
+
function copyCrushOnlyKeys({ from, to }) {
|
|
32766
|
+
for (const { canonical, crush, accepts } of CRUSH_ONLY_KEYS) if (accepts(from[canonical])) to[crush] = from[canonical];
|
|
32767
|
+
}
|
|
32768
|
+
/**
|
|
32769
|
+
* The remote transports Crush's `MCPType` enum accepts, spelled the way it
|
|
32770
|
+
* reads them. `streamable-http` is the canonical rulesync name for what Crush
|
|
32771
|
+
* calls `http`; a WebSocket server has no Crush transport and is left to the
|
|
32772
|
+
* caller to reject.
|
|
32773
|
+
*/
|
|
32774
|
+
function asCrushRemoteType(stated, url) {
|
|
32775
|
+
if (stated === "sse") return "sse";
|
|
32776
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
32777
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
32778
|
+
}
|
|
32779
|
+
/**
|
|
32780
|
+
* The transport half of a remote server (`type`, `url`, `headers`), or null
|
|
32781
|
+
* when Crush could not reach it and the server was skipped with a warning.
|
|
32782
|
+
*/
|
|
32783
|
+
function convertRemoteTransport({ name, config, logger }) {
|
|
32784
|
+
const url = resolveRemoteMcpUrl(config);
|
|
32785
|
+
if (!url) {
|
|
32786
|
+
warnAndSkipMcpServer({
|
|
32787
|
+
toolName: "Crush",
|
|
32788
|
+
serverName: name,
|
|
32789
|
+
reason: "a remote transport without a url",
|
|
32790
|
+
logger
|
|
32791
|
+
});
|
|
32792
|
+
return null;
|
|
32793
|
+
}
|
|
32794
|
+
const stated = config.type ?? config.transport;
|
|
32795
|
+
const type = asCrushRemoteType(stated, url);
|
|
32796
|
+
if (type === void 0) {
|
|
32797
|
+
warnAndSkipMcpServer({
|
|
32798
|
+
toolName: "Crush",
|
|
32799
|
+
serverName: name,
|
|
32800
|
+
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)`,
|
|
32801
|
+
logger
|
|
32802
|
+
});
|
|
32803
|
+
return null;
|
|
32804
|
+
}
|
|
32805
|
+
const converted = {
|
|
32806
|
+
type,
|
|
32807
|
+
url
|
|
32808
|
+
};
|
|
32809
|
+
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
32810
|
+
return converted;
|
|
32811
|
+
}
|
|
32812
|
+
/**
|
|
32813
|
+
* The transport half of a stdio server (`type`, `command`, `args`, `env`), or
|
|
32814
|
+
* null when it has no command and was skipped with a warning.
|
|
32815
|
+
*/
|
|
32816
|
+
function convertStdioTransport({ name, config, logger }) {
|
|
32817
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
32818
|
+
if (!command) {
|
|
32819
|
+
warnAndSkipMcpServer({
|
|
32820
|
+
toolName: "Crush",
|
|
32821
|
+
serverName: name,
|
|
32822
|
+
reason: "a stdio transport without a command",
|
|
32823
|
+
logger
|
|
32824
|
+
});
|
|
32825
|
+
return null;
|
|
32826
|
+
}
|
|
32827
|
+
const converted = {
|
|
32828
|
+
type: "stdio",
|
|
32829
|
+
command
|
|
32830
|
+
};
|
|
32831
|
+
if (args.length > 0) converted.args = args;
|
|
32832
|
+
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
32833
|
+
return converted;
|
|
32834
|
+
}
|
|
32835
|
+
/**
|
|
32836
|
+
* Convert canonical rulesync servers to the `mcp.<name>` shape of crush.json.
|
|
32837
|
+
* `type` is required by Crush's schema, so it is always written: stdio servers
|
|
32838
|
+
* carry `type: "stdio"` with `command`/`args`/`env`, remote servers `http` or
|
|
32839
|
+
* `sse` with `url` and optional `headers`. `disabled`, the per-server
|
|
32840
|
+
* `disabledTools`/`enabledTools` filters (`disabled_tools`/`enabled_tools`)
|
|
32841
|
+
* and `timeout` (which Crush reads in seconds) map onto their Crush fields.
|
|
32842
|
+
*
|
|
32843
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
32844
|
+
*/
|
|
32845
|
+
function convertToCrushFormat(mcpServers, logger) {
|
|
32846
|
+
const result = {};
|
|
32847
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
32848
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
32849
|
+
if (!isRecord$1(config)) continue;
|
|
32850
|
+
if (declaresNoTransport(config)) {
|
|
32851
|
+
warnAndSkipMcpServer({
|
|
32852
|
+
toolName: "Crush",
|
|
32853
|
+
serverName: name,
|
|
32854
|
+
reason: "no transport",
|
|
32855
|
+
logger
|
|
32856
|
+
});
|
|
32857
|
+
continue;
|
|
32858
|
+
}
|
|
32859
|
+
const converted = isRemoteMcpServer(config) ? convertRemoteTransport({
|
|
32860
|
+
name,
|
|
32861
|
+
config,
|
|
32862
|
+
logger
|
|
32863
|
+
}) : convertStdioTransport({
|
|
32864
|
+
name,
|
|
32865
|
+
config,
|
|
32866
|
+
logger
|
|
32867
|
+
});
|
|
32868
|
+
if (converted === null) continue;
|
|
32869
|
+
if (config.disabled === true) converted.disabled = true;
|
|
32870
|
+
if (config.disabledTools && config.disabledTools.length > 0) converted.disabled_tools = config.disabledTools;
|
|
32871
|
+
if (config.enabledTools && config.enabledTools.length > 0) converted.enabled_tools = config.enabledTools;
|
|
32872
|
+
if (typeof config.timeout === "number") converted.timeout = Math.ceil(config.timeout);
|
|
32873
|
+
copyCrushOnlyKeys({
|
|
32874
|
+
from: config,
|
|
32875
|
+
to: converted
|
|
32876
|
+
});
|
|
32877
|
+
result[name] = converted;
|
|
32878
|
+
}
|
|
32879
|
+
return result;
|
|
32880
|
+
}
|
|
32881
|
+
/**
|
|
32882
|
+
* Convert the `mcp.<name>` entries of crush.json back to canonical rulesync
|
|
32883
|
+
* servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
|
|
32884
|
+
* server with a `command` is stdio already); `http` and `sse` are kept as the
|
|
32885
|
+
* canonical transport names; `disabled_tools`/`enabled_tools` become the
|
|
32886
|
+
* canonical `disabledTools`/`enabledTools`; the Crush-only OAuth and
|
|
32887
|
+
* `sessionless` fields are lifted into their `crush*` authoring keys (a value
|
|
32888
|
+
* of the wrong type is dropped, as Crush itself would refuse it), and any
|
|
32889
|
+
* other key is kept verbatim so an import preserves what a hand-authored
|
|
32890
|
+
* entry declared.
|
|
32891
|
+
*/
|
|
32892
|
+
function convertFromCrushFormat(crushServers) {
|
|
32893
|
+
const result = {};
|
|
32894
|
+
for (const [name, config] of Object.entries(crushServers)) {
|
|
32895
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
32896
|
+
const converted = {};
|
|
32897
|
+
for (const [key, value] of Object.entries(config)) {
|
|
32898
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32899
|
+
switch (key) {
|
|
32900
|
+
case "type":
|
|
32901
|
+
if (value === "http" || value === "sse") converted.type = value;
|
|
32902
|
+
break;
|
|
32903
|
+
case "disabled_tools":
|
|
32904
|
+
if (isStringArray$2(value)) converted.disabledTools = value;
|
|
32905
|
+
break;
|
|
32906
|
+
case "enabled_tools":
|
|
32907
|
+
if (isStringArray$2(value)) converted.enabledTools = value;
|
|
32908
|
+
break;
|
|
32909
|
+
case "oauth_token": break;
|
|
32910
|
+
default: {
|
|
32911
|
+
const crushOnly = CRUSH_ONLY_KEYS.find((entry) => entry.crush === key);
|
|
32912
|
+
if (crushOnly === void 0) converted[key] = value;
|
|
32913
|
+
else if (crushOnly.accepts(value)) converted[crushOnly.canonical] = value;
|
|
32914
|
+
}
|
|
32915
|
+
}
|
|
32916
|
+
}
|
|
32917
|
+
result[name] = converted;
|
|
32918
|
+
}
|
|
32919
|
+
return result;
|
|
32920
|
+
}
|
|
32921
|
+
/**
|
|
32922
|
+
* Crush MCP servers.
|
|
32923
|
+
*
|
|
32924
|
+
* Crush reads MCP servers from the `mcp` key of its JSON config:
|
|
32925
|
+
* `<project>/crush.json` (or an existing `.crush.json`; Crush merges the pair,
|
|
32926
|
+
* objects recursively) at project scope and `~/.config/crush/crush.json` at
|
|
32927
|
+
* user scope. The
|
|
32928
|
+
* `crushrc` Bash config that Crush now recommends compiles its `mcp add`
|
|
32929
|
+
* builtin into the same `mcp.<name>` entries and overrides the JSON key by
|
|
32930
|
+
* key, so a `crushrc` next to the generated file takes precedence. Every
|
|
32931
|
+
* other top-level key of the file is preserved; the file is never deleted.
|
|
32932
|
+
*
|
|
32933
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
32934
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
32935
|
+
*/
|
|
32936
|
+
var CrushMcp = class CrushMcp extends ToolMcp {
|
|
32937
|
+
json;
|
|
32938
|
+
constructor(params) {
|
|
32939
|
+
super(params);
|
|
32940
|
+
this.json = parseCrushConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
32941
|
+
}
|
|
32942
|
+
getJson() {
|
|
32943
|
+
return this.json;
|
|
32944
|
+
}
|
|
32945
|
+
isDeletable() {
|
|
32946
|
+
return false;
|
|
32947
|
+
}
|
|
32948
|
+
static getSettablePaths({ global = false } = {}) {
|
|
32949
|
+
return getCrushConfigSettablePaths({ global });
|
|
32950
|
+
}
|
|
32951
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32952
|
+
const location = await resolveCrushConfigFile({
|
|
32953
|
+
outputRoot,
|
|
32954
|
+
global
|
|
32955
|
+
});
|
|
32956
|
+
return new CrushMcp({
|
|
32957
|
+
outputRoot,
|
|
32958
|
+
relativeDirPath: location.relativeDirPath,
|
|
32959
|
+
relativeFilePath: location.relativeFilePath,
|
|
32960
|
+
fileContent: crushConfigImportContent(location),
|
|
32961
|
+
validate,
|
|
32962
|
+
global
|
|
32963
|
+
});
|
|
32964
|
+
}
|
|
32965
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
32966
|
+
const location = await resolveCrushConfigFile({
|
|
32967
|
+
outputRoot,
|
|
32968
|
+
global
|
|
32969
|
+
});
|
|
32970
|
+
const existingContent = location.fileContent ?? "";
|
|
32971
|
+
warnCrushTwinLeftovers({
|
|
32972
|
+
location,
|
|
32973
|
+
ownedPaths: [["mcp"]],
|
|
32974
|
+
logger
|
|
32975
|
+
});
|
|
32976
|
+
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
|
|
32977
|
+
...serverConfig,
|
|
32978
|
+
...readCrushOnlyKeys(rulesyncMcp.getRawMcpServer(serverName))
|
|
32979
|
+
}])), logger);
|
|
32980
|
+
return new CrushMcp({
|
|
32981
|
+
outputRoot,
|
|
32982
|
+
relativeDirPath: location.relativeDirPath,
|
|
32983
|
+
relativeFilePath: location.relativeFilePath,
|
|
32984
|
+
fileContent: applySharedConfigPatch({
|
|
32985
|
+
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
32986
|
+
feature: "mcp",
|
|
32987
|
+
existingContent,
|
|
32988
|
+
patch: { ["mcp"]: converted },
|
|
32989
|
+
filePath: location.filePath,
|
|
32990
|
+
logger
|
|
32991
|
+
}),
|
|
32992
|
+
validate,
|
|
32993
|
+
global
|
|
32994
|
+
});
|
|
32995
|
+
}
|
|
32996
|
+
toRulesyncMcp() {
|
|
32997
|
+
const converted = convertFromCrushFormat(isRecord$1(this.json["mcp"]) ? this.json["mcp"] : {});
|
|
32998
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
32999
|
+
}
|
|
33000
|
+
validate() {
|
|
33001
|
+
return {
|
|
33002
|
+
success: true,
|
|
33003
|
+
error: null
|
|
33004
|
+
};
|
|
33005
|
+
}
|
|
33006
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
33007
|
+
return new CrushMcp({
|
|
33008
|
+
outputRoot,
|
|
33009
|
+
relativeDirPath,
|
|
33010
|
+
relativeFilePath,
|
|
33011
|
+
fileContent: JSON.stringify({ ["mcp"]: {} }, null, 2),
|
|
33012
|
+
validate: false,
|
|
33013
|
+
global
|
|
33014
|
+
});
|
|
33015
|
+
}
|
|
33016
|
+
};
|
|
33017
|
+
//#endregion
|
|
32424
33018
|
//#region src/features/mcp/mcp-env-var-format.ts
|
|
32425
33019
|
/**
|
|
32426
33020
|
* Canonical rulesync env var reference pattern: `${VAR}` (but not `${env:VAR}`,
|
|
@@ -36465,7 +37059,7 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36465
37059
|
}
|
|
36466
37060
|
for (const [key, value] of Object.entries(rest)) {
|
|
36467
37061
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36468
|
-
converted[key] = (
|
|
37062
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36469
37063
|
}
|
|
36470
37064
|
if (enabledTools !== void 0) converted.includeTools = enabledTools;
|
|
36471
37065
|
if (disabledTools !== void 0) converted.excludeTools = disabledTools;
|
|
@@ -36476,7 +37070,9 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36476
37070
|
/**
|
|
36477
37071
|
* Convert Tabnine's server map back to the canonical shape: `includeTools` /
|
|
36478
37072
|
* `excludeTools` become `enabledTools` / `disabledTools`; `type` (`stdio`,
|
|
36479
|
-
* `sse`, `http`) and every other documented field are already canonical
|
|
37073
|
+
* `sse`, `http`) and every other documented field are already canonical and
|
|
37074
|
+
* pass through with their prototype-pollution keys dropped at every nesting
|
|
37075
|
+
* level, mirroring the generate side.
|
|
36480
37076
|
*/
|
|
36481
37077
|
function convertFromTabnineFormat(mcpServers) {
|
|
36482
37078
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -36488,7 +37084,7 @@ function convertFromTabnineFormat(mcpServers) {
|
|
|
36488
37084
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36489
37085
|
if (key === "includeTools") converted.enabledTools = value;
|
|
36490
37086
|
else if (key === "excludeTools") converted.disabledTools = value;
|
|
36491
|
-
else converted[key] = value;
|
|
37087
|
+
else converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36492
37088
|
}
|
|
36493
37089
|
result[serverName] = converted;
|
|
36494
37090
|
}
|
|
@@ -37636,6 +38232,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37636
38232
|
supportsDisabledTools: false
|
|
37637
38233
|
}
|
|
37638
38234
|
}],
|
|
38235
|
+
["crush", {
|
|
38236
|
+
class: CrushMcp,
|
|
38237
|
+
meta: {
|
|
38238
|
+
supportsProject: true,
|
|
38239
|
+
supportsGlobal: true,
|
|
38240
|
+
supportsEnabledTools: true,
|
|
38241
|
+
supportsDisabledTools: true
|
|
38242
|
+
}
|
|
38243
|
+
}],
|
|
37639
38244
|
["cursor", {
|
|
37640
38245
|
class: CursorMcp,
|
|
37641
38246
|
meta: {
|
|
@@ -39067,7 +39672,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
39067
39672
|
function toAntigravityCliToolName(canonical) {
|
|
39068
39673
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
39069
39674
|
}
|
|
39070
|
-
function toCanonicalToolName$
|
|
39675
|
+
function toCanonicalToolName$8(cliName) {
|
|
39071
39676
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
39072
39677
|
}
|
|
39073
39678
|
/**
|
|
@@ -39283,7 +39888,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
39283
39888
|
const processEntries = (entries, action) => {
|
|
39284
39889
|
for (const entry of entries) {
|
|
39285
39890
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
39286
|
-
const canonical = toCanonicalToolName$
|
|
39891
|
+
const canonical = toCanonicalToolName$8(toolName);
|
|
39287
39892
|
if (!permission[canonical]) permission[canonical] = {};
|
|
39288
39893
|
permission[canonical][pattern] = action;
|
|
39289
39894
|
}
|
|
@@ -39540,11 +40145,34 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
|
|
|
39540
40145
|
websearch: "web-search"
|
|
39541
40146
|
};
|
|
39542
40147
|
const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
40148
|
+
const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
|
|
40149
|
+
terminal: "launch-process",
|
|
40150
|
+
read: "view",
|
|
40151
|
+
edit: "str-replace-editor",
|
|
40152
|
+
write: "save-file"
|
|
40153
|
+
};
|
|
39543
40154
|
function toAugmentToolName(canonical) {
|
|
39544
|
-
return
|
|
40155
|
+
return lookupOwn({
|
|
40156
|
+
record: CANONICAL_TO_AUGMENT_TOOL_NAMES,
|
|
40157
|
+
key: canonical
|
|
40158
|
+
}) ?? canonical;
|
|
40159
|
+
}
|
|
40160
|
+
/**
|
|
40161
|
+
* Resolve an existing entry's `toolName` to the legacy spelling rulesync emits, so that the
|
|
40162
|
+
* current alias and the legacy name are treated as the same managed tool.
|
|
40163
|
+
*/
|
|
40164
|
+
function toLegacyAugmentToolName(augmentName) {
|
|
40165
|
+
return lookupOwn({
|
|
40166
|
+
record: CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES,
|
|
40167
|
+
key: augmentName
|
|
40168
|
+
}) ?? augmentName;
|
|
39545
40169
|
}
|
|
39546
|
-
function toCanonicalToolName$
|
|
39547
|
-
|
|
40170
|
+
function toCanonicalToolName$7(augmentName) {
|
|
40171
|
+
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
40172
|
+
return lookupOwn({
|
|
40173
|
+
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
40174
|
+
key: legacyName
|
|
40175
|
+
}) ?? legacyName;
|
|
39548
40176
|
}
|
|
39549
40177
|
function actionToAugmentType(action) {
|
|
39550
40178
|
switch (action) {
|
|
@@ -39760,10 +40388,11 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
39760
40388
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
39761
40389
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
39762
40390
|
if (entry.toolName === "*") return false;
|
|
39763
|
-
|
|
40391
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40392
|
+
if (!MANAGED_AUGMENT_TOOL_NAMES.has(legacyToolName)) return true;
|
|
39764
40393
|
if (entry.permission.type === "deny") {
|
|
39765
|
-
const
|
|
39766
|
-
return !generatedKeys.has(
|
|
40394
|
+
const suffix = `|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
40395
|
+
return !generatedKeys.has(`${entry.toolName}${suffix}`) && !generatedKeys.has(`${legacyToolName}${suffix}`);
|
|
39767
40396
|
}
|
|
39768
40397
|
return false;
|
|
39769
40398
|
});
|
|
@@ -39990,14 +40619,15 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
39990
40619
|
if (isSpecialEntry(entry)) continue;
|
|
39991
40620
|
const type = entry.permission.type;
|
|
39992
40621
|
if (!isBasicAugmentType(type)) continue;
|
|
39993
|
-
const
|
|
40622
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40623
|
+
const canonical = toCanonicalToolName$7(entry.toolName);
|
|
39994
40624
|
if (forbiddenMapKeys.has(canonical)) {
|
|
39995
40625
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
|
|
39996
40626
|
continue;
|
|
39997
40627
|
}
|
|
39998
40628
|
const action = augmentTypeToAction(type);
|
|
39999
40629
|
let pattern;
|
|
40000
|
-
if (
|
|
40630
|
+
if (legacyToolName === "launch-process" && entry.shellInputRegex) {
|
|
40001
40631
|
const regex = entry.shellInputRegex;
|
|
40002
40632
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
40003
40633
|
else if (action === "deny") {
|
|
@@ -40012,9 +40642,19 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40012
40642
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because its pattern resolves to the reserved object key '${pattern}'.`);
|
|
40013
40643
|
continue;
|
|
40014
40644
|
}
|
|
40015
|
-
|
|
40016
|
-
|
|
40017
|
-
|
|
40645
|
+
let bucket = lookupOwn({
|
|
40646
|
+
record: permission,
|
|
40647
|
+
key: canonical
|
|
40648
|
+
});
|
|
40649
|
+
if (bucket === void 0) {
|
|
40650
|
+
bucket = {};
|
|
40651
|
+
permission[canonical] = bucket;
|
|
40652
|
+
}
|
|
40653
|
+
const existing = lookupOwn({
|
|
40654
|
+
record: bucket,
|
|
40655
|
+
key: pattern
|
|
40656
|
+
});
|
|
40657
|
+
if (existing === void 0 || actionPriority[action] > actionPriority[existing]) bucket[pattern] = action;
|
|
40018
40658
|
}
|
|
40019
40659
|
return { permission };
|
|
40020
40660
|
}
|
|
@@ -40225,7 +40865,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
40225
40865
|
function toClaudeToolName(canonical) {
|
|
40226
40866
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
40227
40867
|
}
|
|
40228
|
-
function toCanonicalToolName$
|
|
40868
|
+
function toCanonicalToolName$6(claudeName) {
|
|
40229
40869
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
40230
40870
|
}
|
|
40231
40871
|
/**
|
|
@@ -41120,7 +41760,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
41120
41760
|
const processEntries = (entries, action) => {
|
|
41121
41761
|
for (const entry of entries) {
|
|
41122
41762
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
41123
|
-
const canonical = toCanonicalToolName$
|
|
41763
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
41124
41764
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41125
41765
|
permission[canonical][pattern] = action;
|
|
41126
41766
|
}
|
|
@@ -41945,7 +42585,16 @@ function mapBashActionToDecision(action) {
|
|
|
41945
42585
|
//#endregion
|
|
41946
42586
|
//#region src/features/permissions/continue-permissions.ts
|
|
41947
42587
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
41948
|
-
const CATCH_ALL_PATTERN$
|
|
42588
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
42589
|
+
/**
|
|
42590
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
42591
|
+
* would match `Tool(**)` against every argument just as the bare tool name
|
|
42592
|
+
* does, so it is written — and, under the all-tools category, honored — as
|
|
42593
|
+
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
42594
|
+
*/
|
|
42595
|
+
function normalizeCatchAllPattern$1(pattern) {
|
|
42596
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
42597
|
+
}
|
|
41949
42598
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
41950
42599
|
bash: "Bash",
|
|
41951
42600
|
read: "Read",
|
|
@@ -41973,7 +42622,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
41973
42622
|
function toContinueToolName(canonical) {
|
|
41974
42623
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
41975
42624
|
}
|
|
41976
|
-
function toCanonicalToolName$
|
|
42625
|
+
function toCanonicalToolName$5(continueName) {
|
|
41977
42626
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
41978
42627
|
}
|
|
41979
42628
|
/**
|
|
@@ -41981,7 +42630,7 @@ function toCanonicalToolName$4(continueName) {
|
|
|
41981
42630
|
* `Tool(pattern)`.
|
|
41982
42631
|
*/
|
|
41983
42632
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
41984
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
42633
|
+
return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
|
|
41985
42634
|
}
|
|
41986
42635
|
/**
|
|
41987
42636
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -41995,7 +42644,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
41995
42644
|
if (toolName === "") return;
|
|
41996
42645
|
return {
|
|
41997
42646
|
toolName,
|
|
41998
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
42647
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
|
|
41999
42648
|
};
|
|
42000
42649
|
}
|
|
42001
42650
|
/**
|
|
@@ -42162,13 +42811,14 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
42162
42811
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42163
42812
|
if (isPrototypePollutionKey(category)) continue;
|
|
42164
42813
|
const toolName = category === "*" ? "*" : toContinueToolName(category);
|
|
42165
|
-
for (const [
|
|
42166
|
-
if (isPrototypePollutionKey(
|
|
42814
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
42815
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
42816
|
+
const pattern = normalizeCatchAllPattern$1(rawPattern);
|
|
42167
42817
|
if (pattern.includes("(") || pattern.includes(")")) {
|
|
42168
42818
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42169
42819
|
continue;
|
|
42170
42820
|
}
|
|
42171
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
42821
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
|
|
42172
42822
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42173
42823
|
continue;
|
|
42174
42824
|
}
|
|
@@ -42193,7 +42843,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
42193
42843
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
42194
42844
|
if (parsedEntry === void 0) continue;
|
|
42195
42845
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
42196
|
-
const category = toCanonicalToolName$
|
|
42846
|
+
const category = toCanonicalToolName$5(parsedEntry.toolName);
|
|
42197
42847
|
permission[category] ??= {};
|
|
42198
42848
|
permission[category][parsedEntry.pattern] ??= action;
|
|
42199
42849
|
}
|
|
@@ -42516,6 +43166,295 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
42516
43166
|
}
|
|
42517
43167
|
};
|
|
42518
43168
|
//#endregion
|
|
43169
|
+
//#region src/features/permissions/crush-permissions.ts
|
|
43170
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
43171
|
+
/**
|
|
43172
|
+
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
43173
|
+
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
43174
|
+
* still be written.
|
|
43175
|
+
*/
|
|
43176
|
+
function normalizeCatchAllPattern(pattern) {
|
|
43177
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
|
|
43178
|
+
}
|
|
43179
|
+
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
43180
|
+
bash: "bash",
|
|
43181
|
+
read: "view",
|
|
43182
|
+
edit: "edit",
|
|
43183
|
+
write: "write",
|
|
43184
|
+
grep: "grep",
|
|
43185
|
+
glob: "glob",
|
|
43186
|
+
webfetch: "fetch"
|
|
43187
|
+
};
|
|
43188
|
+
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
43189
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
43190
|
+
const MCP_CRUSH_PREFIX = "mcp_";
|
|
43191
|
+
function toCrushToolName(canonical) {
|
|
43192
|
+
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
43193
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
43194
|
+
return canonical;
|
|
43195
|
+
}
|
|
43196
|
+
function toCanonicalToolName$4(crushName) {
|
|
43197
|
+
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
43198
|
+
}
|
|
43199
|
+
function isMcpToolName(crushName) {
|
|
43200
|
+
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
43201
|
+
}
|
|
43202
|
+
function uniq$1(values) {
|
|
43203
|
+
return [...new Set(values)];
|
|
43204
|
+
}
|
|
43205
|
+
/**
|
|
43206
|
+
* Permissions adapter for Crush.
|
|
43207
|
+
*
|
|
43208
|
+
* Crush has no per-argument permission rules. Its JSON config carries two
|
|
43209
|
+
* tool-level lists instead:
|
|
43210
|
+
* - `permissions.allowed_tools`: tools (or `tool:action` pairs) that run
|
|
43211
|
+
* without a permission prompt; everything else prompts.
|
|
43212
|
+
* - `options.disabled_tools`: built-in tools removed from the agent
|
|
43213
|
+
* entirely.
|
|
43214
|
+
*
|
|
43215
|
+
* Mapping (rulesync canonical -> Crush), catch-all (`*`) rules only:
|
|
43216
|
+
* - `allow` -> an `allowed_tools` entry.
|
|
43217
|
+
* - `deny` -> a `disabled_tools` entry. Crush only filters its built-ins
|
|
43218
|
+
* through that list, so a deny for an MCP tool is reported and skipped
|
|
43219
|
+
* (disable the tool in the MCP server's `disabled_tools` instead).
|
|
43220
|
+
* - `ask` -> nothing (prompting is Crush's default).
|
|
43221
|
+
* - Tool name: `bash` -> `bash`, `read` -> `view`, `edit` -> `edit`,
|
|
43222
|
+
* `write` -> `write`, `grep` -> `grep`, `glob` -> `glob`,
|
|
43223
|
+
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
43224
|
+
* any other category passes through verbatim.
|
|
43225
|
+
*
|
|
43226
|
+
* A pattern-specific rule cannot be expressed and is reported and skipped.
|
|
43227
|
+
* Because Crush cannot narrow an allowed tool, a category's catch-all `allow`
|
|
43228
|
+
* is not written while the same category carries a pattern-specific `deny` or
|
|
43229
|
+
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
43230
|
+
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
43231
|
+
* `honorAllToolsOnBash` and otherwise skipped, since Crush has no allow-all
|
|
43232
|
+
* or disable-all list.
|
|
43233
|
+
*
|
|
43234
|
+
* `crush.json` is Crush's main config: entries naming a tool the canonical
|
|
43235
|
+
* config does not manage, and every `tool:action` entry (a form rulesync
|
|
43236
|
+
* never derives), are preserved verbatim; the managed tools' bare entries
|
|
43237
|
+
* are rebuilt, every other key of `permissions` / `options` and of the file is
|
|
43238
|
+
* kept, and the file is never deleted. Project scope writes `crush.json`, or
|
|
43239
|
+
* an existing `.crush.json`; Crush merges the pair (lists concatenated), so an
|
|
43240
|
+
* entry left in the other file stays in effect and is reported.
|
|
43241
|
+
*
|
|
43242
|
+
* @see https://github.com/charmbracelet/crush/blob/main/docs/config/README.md
|
|
43243
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/config.go
|
|
43244
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/permission/permission.go
|
|
43245
|
+
*/
|
|
43246
|
+
var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
43247
|
+
json;
|
|
43248
|
+
constructor(params) {
|
|
43249
|
+
super({
|
|
43250
|
+
...params,
|
|
43251
|
+
fileContent: params.fileContent ?? ""
|
|
43252
|
+
});
|
|
43253
|
+
this.json = parseCrushConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
43254
|
+
}
|
|
43255
|
+
getJson() {
|
|
43256
|
+
return this.json;
|
|
43257
|
+
}
|
|
43258
|
+
isDeletable() {
|
|
43259
|
+
return false;
|
|
43260
|
+
}
|
|
43261
|
+
/**
|
|
43262
|
+
* `crush.json` is Crush's file: rulesync merges into it when it exists but
|
|
43263
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
43264
|
+
* absent `allowed_tools` both mean "prompt for everything".
|
|
43265
|
+
*/
|
|
43266
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
43267
|
+
return true;
|
|
43268
|
+
}
|
|
43269
|
+
static getSettablePaths({ global = false } = {}) {
|
|
43270
|
+
return getCrushConfigSettablePaths({ global });
|
|
43271
|
+
}
|
|
43272
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
43273
|
+
const location = await resolveCrushConfigFile({
|
|
43274
|
+
outputRoot,
|
|
43275
|
+
global
|
|
43276
|
+
});
|
|
43277
|
+
return new CrushPermissions({
|
|
43278
|
+
outputRoot,
|
|
43279
|
+
relativeDirPath: location.relativeDirPath,
|
|
43280
|
+
relativeFilePath: location.relativeFilePath,
|
|
43281
|
+
fileContent: crushConfigImportContent(location),
|
|
43282
|
+
validate,
|
|
43283
|
+
global
|
|
43284
|
+
});
|
|
43285
|
+
}
|
|
43286
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
43287
|
+
const location = await resolveCrushConfigFile({
|
|
43288
|
+
outputRoot,
|
|
43289
|
+
global
|
|
43290
|
+
});
|
|
43291
|
+
const existingContent = location.fileContent ?? "";
|
|
43292
|
+
warnCrushTwinLeftovers({
|
|
43293
|
+
location,
|
|
43294
|
+
ownedPaths: [[CRUSH_PERMISSIONS_KEY, CRUSH_ALLOWED_TOOLS_KEY], [CRUSH_OPTIONS_KEY, CRUSH_DISABLED_TOOLS_KEY]],
|
|
43295
|
+
logger
|
|
43296
|
+
});
|
|
43297
|
+
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
43298
|
+
const existingPermissions = isRecord$1(existing["permissions"]) ? existing[CRUSH_PERMISSIONS_KEY] : void 0;
|
|
43299
|
+
const existingOptions = isRecord$1(existing["options"]) ? existing[CRUSH_OPTIONS_KEY] : void 0;
|
|
43300
|
+
const config = rulesyncPermissions.getJson();
|
|
43301
|
+
const generated = convertRulesyncToCrushLists({
|
|
43302
|
+
config,
|
|
43303
|
+
logger
|
|
43304
|
+
});
|
|
43305
|
+
const managedToolNames = managedCrushToolNames(config);
|
|
43306
|
+
const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
43307
|
+
const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
43308
|
+
const allowedList = uniq$1([...preservedAllowed, ...generated.allowed]);
|
|
43309
|
+
const disabledList = uniq$1([...preservedDisabled, ...generated.disabled]);
|
|
43310
|
+
const patch = {};
|
|
43311
|
+
if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
|
|
43312
|
+
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
43313
|
+
return new CrushPermissions({
|
|
43314
|
+
outputRoot,
|
|
43315
|
+
relativeDirPath: location.relativeDirPath,
|
|
43316
|
+
relativeFilePath: location.relativeFilePath,
|
|
43317
|
+
fileContent: applySharedConfigPatch({
|
|
43318
|
+
fileKey: sharedConfigFileKey(CrushPermissions.getSettablePaths({ global })),
|
|
43319
|
+
feature: "permissions",
|
|
43320
|
+
existingContent,
|
|
43321
|
+
patch,
|
|
43322
|
+
filePath: location.filePath,
|
|
43323
|
+
logger
|
|
43324
|
+
}),
|
|
43325
|
+
validate: true,
|
|
43326
|
+
global
|
|
43327
|
+
});
|
|
43328
|
+
}
|
|
43329
|
+
toRulesyncPermissions() {
|
|
43330
|
+
const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
43331
|
+
const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
|
|
43332
|
+
const rulesyncConfig = convertCrushListsToRulesync({
|
|
43333
|
+
allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
|
|
43334
|
+
disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
|
|
43335
|
+
});
|
|
43336
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
43337
|
+
}
|
|
43338
|
+
validate() {
|
|
43339
|
+
return {
|
|
43340
|
+
success: true,
|
|
43341
|
+
error: null
|
|
43342
|
+
};
|
|
43343
|
+
}
|
|
43344
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
43345
|
+
return new CrushPermissions({
|
|
43346
|
+
outputRoot,
|
|
43347
|
+
relativeDirPath,
|
|
43348
|
+
relativeFilePath,
|
|
43349
|
+
fileContent: "{}",
|
|
43350
|
+
validate: false,
|
|
43351
|
+
global
|
|
43352
|
+
});
|
|
43353
|
+
}
|
|
43354
|
+
};
|
|
43355
|
+
/**
|
|
43356
|
+
* The Crush tool names the canonical config manages, i.e. the names its
|
|
43357
|
+
* categories map to. Entries naming any other tool are the user's and survive
|
|
43358
|
+
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
43359
|
+
*/
|
|
43360
|
+
function managedCrushToolNames(config) {
|
|
43361
|
+
return new Set(Object.keys(config.permission).filter((category) => category !== "*" && !isPrototypePollutionKey(category)).map((category) => toCrushToolName(category)));
|
|
43362
|
+
}
|
|
43363
|
+
/**
|
|
43364
|
+
* Reduce one category's rules to the single tool-wide action Crush can carry,
|
|
43365
|
+
* or undefined when nothing should be written for the tool: no catch-all rule,
|
|
43366
|
+
* a catch-all allow next to a narrower deny/ask (Crush cannot narrow an
|
|
43367
|
+
* allowed tool, so it keeps prompting instead), or a deny on an MCP tool
|
|
43368
|
+
* (`options.disabled_tools` only covers built-ins).
|
|
43369
|
+
*/
|
|
43370
|
+
function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
43371
|
+
let catchAllAction;
|
|
43372
|
+
let restricted = false;
|
|
43373
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
43374
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
43375
|
+
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
43376
|
+
if (pattern === CATCH_ALL_PATTERN$4) {
|
|
43377
|
+
catchAllAction = action;
|
|
43378
|
+
continue;
|
|
43379
|
+
}
|
|
43380
|
+
logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
43381
|
+
if (action !== "allow") restricted = true;
|
|
43382
|
+
}
|
|
43383
|
+
if (catchAllAction === "allow" && restricted) {
|
|
43384
|
+
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}".`);
|
|
43385
|
+
return;
|
|
43386
|
+
}
|
|
43387
|
+
if (catchAllAction === "deny" && isMcpToolName(toolName)) {
|
|
43388
|
+
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.`);
|
|
43389
|
+
return;
|
|
43390
|
+
}
|
|
43391
|
+
return catchAllAction;
|
|
43392
|
+
}
|
|
43393
|
+
/**
|
|
43394
|
+
* Convert a rulesync permissions config into Crush's two tool lists.
|
|
43395
|
+
*/
|
|
43396
|
+
function convertRulesyncToCrushLists({ config, logger }) {
|
|
43397
|
+
const lists = {
|
|
43398
|
+
allowed: [],
|
|
43399
|
+
disabled: []
|
|
43400
|
+
};
|
|
43401
|
+
const actionByTool = /* @__PURE__ */ new Map();
|
|
43402
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
43403
|
+
if (isPrototypePollutionKey(category)) continue;
|
|
43404
|
+
if (category === "*") {
|
|
43405
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
43406
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
43407
|
+
logger?.warn(`Crush has no allow-all or disable-all tool list, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
43408
|
+
}
|
|
43409
|
+
continue;
|
|
43410
|
+
}
|
|
43411
|
+
const toolName = toCrushToolName(category);
|
|
43412
|
+
const catchAllAction = resolveCategoryAction({
|
|
43413
|
+
category,
|
|
43414
|
+
toolName,
|
|
43415
|
+
rules,
|
|
43416
|
+
logger
|
|
43417
|
+
});
|
|
43418
|
+
if (catchAllAction === void 0) continue;
|
|
43419
|
+
const previous = actionByTool.get(toolName);
|
|
43420
|
+
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.`);
|
|
43421
|
+
actionByTool.set(toolName, catchAllAction);
|
|
43422
|
+
if (catchAllAction === "allow") lists.allowed.push(toolName);
|
|
43423
|
+
else if (catchAllAction === "deny") lists.disabled.push(toolName);
|
|
43424
|
+
}
|
|
43425
|
+
return {
|
|
43426
|
+
allowed: uniq$1(lists.allowed),
|
|
43427
|
+
disabled: uniq$1(lists.disabled)
|
|
43428
|
+
};
|
|
43429
|
+
}
|
|
43430
|
+
/**
|
|
43431
|
+
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
43432
|
+
* entry scopes an allow to one action of a tool, which the canonical model
|
|
43433
|
+
* cannot express, so it is left out rather than widened to the whole tool.
|
|
43434
|
+
*/
|
|
43435
|
+
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
43436
|
+
const permission = {};
|
|
43437
|
+
const bucketFor = (category) => {
|
|
43438
|
+
const own = lookupOwn({
|
|
43439
|
+
record: permission,
|
|
43440
|
+
key: category
|
|
43441
|
+
});
|
|
43442
|
+
if (own !== void 0) return own;
|
|
43443
|
+
const created = {};
|
|
43444
|
+
permission[category] = created;
|
|
43445
|
+
return created;
|
|
43446
|
+
};
|
|
43447
|
+
for (const entry of disabled) {
|
|
43448
|
+
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
43449
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
|
|
43450
|
+
}
|
|
43451
|
+
for (const entry of allowed) {
|
|
43452
|
+
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
43453
|
+
bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
|
|
43454
|
+
}
|
|
43455
|
+
return { permission };
|
|
43456
|
+
}
|
|
43457
|
+
//#endregion
|
|
42519
43458
|
//#region src/features/permissions/cursor-permissions.ts
|
|
42520
43459
|
/**
|
|
42521
43460
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -43670,6 +44609,40 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43670
44609
|
return Object.keys(bash).length > 0 ? { permission: { bash } } : { permission: {} };
|
|
43671
44610
|
}
|
|
43672
44611
|
//#endregion
|
|
44612
|
+
//#region src/features/permissions/single-action-collapse.ts
|
|
44613
|
+
/**
|
|
44614
|
+
* Strictness order shared by the tools whose permission model evaluates
|
|
44615
|
+
* `deny > ask > allow`. Used wherever several canonical rules have to fold
|
|
44616
|
+
* into one entry, so the strictest action always wins.
|
|
44617
|
+
*/
|
|
44618
|
+
const PERMISSION_ACTION_PRIORITY = {
|
|
44619
|
+
allow: 0,
|
|
44620
|
+
ask: 1,
|
|
44621
|
+
deny: 2
|
|
44622
|
+
};
|
|
44623
|
+
/**
|
|
44624
|
+
* Collapse a pattern map to the single action a tool can hold for a scope
|
|
44625
|
+
* that has no pattern matcher, using deny > ask > allow precedence.
|
|
44626
|
+
*
|
|
44627
|
+
* A map without a catch-all grants nothing to unmatched inputs, so an
|
|
44628
|
+
* implicit `ask` joins the candidates and a narrow allowlist can never widen
|
|
44629
|
+
* into a blanket allow. Returns `undefined` for an empty map; each caller
|
|
44630
|
+
* decides what an empty map means for its tool (e.g. emit nothing, or fall
|
|
44631
|
+
* back to `deny` when the tool's own default is allow).
|
|
44632
|
+
*/
|
|
44633
|
+
function collapseRulesToSingleAction({ rules }) {
|
|
44634
|
+
const actions = Object.values(rules);
|
|
44635
|
+
if (actions.length === 0) return;
|
|
44636
|
+
return (Object.hasOwn(rules, "*") ? actions : [...actions, "ask"]).reduce((current, candidate) => PERMISSION_ACTION_PRIORITY[candidate] > PERMISSION_ACTION_PRIORITY[current] ? candidate : current);
|
|
44637
|
+
}
|
|
44638
|
+
/**
|
|
44639
|
+
* Whether a pattern map carries anything beyond the `*` catch-all, i.e.
|
|
44640
|
+
* whether collapsing it to a single action loses information worth a warning.
|
|
44641
|
+
*/
|
|
44642
|
+
function hasPatternSpecificRules(rules) {
|
|
44643
|
+
return Object.keys(rules).some((pattern) => pattern !== "*");
|
|
44644
|
+
}
|
|
44645
|
+
//#endregion
|
|
43673
44646
|
//#region src/features/permissions/devin-permissions.ts
|
|
43674
44647
|
/**
|
|
43675
44648
|
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
@@ -43679,17 +44652,21 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43679
44652
|
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
43680
44653
|
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
43681
44654
|
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
43682
|
-
* `edit` rules round-trip as `write` (a lossy but documented collapse).
|
|
44655
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). The
|
|
44656
|
+
* canonical `websearch` category maps onto the bare `web_search` tool name,
|
|
44657
|
+
* accepted in the permission lists since CLI v3000.10.21 (2026-09-10). Unknown
|
|
43683
44658
|
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
43684
44659
|
*
|
|
43685
44660
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
44661
|
+
* @see https://docs.devin.ai/cli/changelog/stable — v3000.10.21, `web_search`
|
|
43686
44662
|
*/
|
|
43687
44663
|
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
43688
44664
|
read: "Read",
|
|
43689
44665
|
write: "Write",
|
|
43690
44666
|
edit: "Write",
|
|
43691
44667
|
bash: "Exec",
|
|
43692
|
-
webfetch: "Fetch"
|
|
44668
|
+
webfetch: "Fetch",
|
|
44669
|
+
websearch: "web_search"
|
|
43693
44670
|
};
|
|
43694
44671
|
/**
|
|
43695
44672
|
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
@@ -43698,8 +44675,15 @@ const DEVIN_SCOPE_TO_CANONICAL = {
|
|
|
43698
44675
|
Read: "read",
|
|
43699
44676
|
Write: "write",
|
|
43700
44677
|
Exec: "bash",
|
|
43701
|
-
Fetch: "webfetch"
|
|
44678
|
+
Fetch: "webfetch",
|
|
44679
|
+
web_search: "websearch"
|
|
43702
44680
|
};
|
|
44681
|
+
/**
|
|
44682
|
+
* Devin scopes that exist only as a bare tool name: there is no
|
|
44683
|
+
* `web_search(pattern)` matcher, so a pattern-specific rule under the canonical
|
|
44684
|
+
* category cannot be expressed and is collapsed to one action instead.
|
|
44685
|
+
*/
|
|
44686
|
+
const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
|
|
43703
44687
|
function toDevinScope(canonical) {
|
|
43704
44688
|
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
43705
44689
|
}
|
|
@@ -43884,7 +44868,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43884
44868
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
43885
44869
|
}
|
|
43886
44870
|
const config = rulesyncPermissions.getJson();
|
|
43887
|
-
const { allow, ask, deny } = convertRulesyncToDevinPermissions(
|
|
44871
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions({
|
|
44872
|
+
config,
|
|
44873
|
+
logger
|
|
44874
|
+
});
|
|
43888
44875
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
43889
44876
|
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
43890
44877
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
@@ -43988,26 +44975,50 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43988
44975
|
}
|
|
43989
44976
|
};
|
|
43990
44977
|
/**
|
|
44978
|
+
* Collapse the pattern rules of a bare-only scope to the single action Devin
|
|
44979
|
+
* can hold for it, using deny > ask > allow precedence. A map without a
|
|
44980
|
+
* catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
|
|
44981
|
+
* candidates and a narrow allowlist can never widen into a blanket allow.
|
|
44982
|
+
* Returns `undefined` for an empty map: like any other empty category it
|
|
44983
|
+
* emits nothing, which leaves Devin's own auto-approve default in place.
|
|
44984
|
+
*/
|
|
44985
|
+
function collapseBareOnlyScopeRules({ category, scope, rules, logger }) {
|
|
44986
|
+
const action = collapseRulesToSingleAction({ rules });
|
|
44987
|
+
if (action === void 0) return;
|
|
44988
|
+
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.`);
|
|
44989
|
+
return action;
|
|
44990
|
+
}
|
|
44991
|
+
/**
|
|
43991
44992
|
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
43992
44993
|
*/
|
|
43993
|
-
function convertRulesyncToDevinPermissions(config) {
|
|
44994
|
+
function convertRulesyncToDevinPermissions({ config, logger }) {
|
|
43994
44995
|
const allow = [];
|
|
43995
44996
|
const ask = [];
|
|
43996
44997
|
const deny = [];
|
|
44998
|
+
const push = (entry, action) => {
|
|
44999
|
+
switch (action) {
|
|
45000
|
+
case "allow":
|
|
45001
|
+
allow.push(entry);
|
|
45002
|
+
break;
|
|
45003
|
+
case "ask":
|
|
45004
|
+
ask.push(entry);
|
|
45005
|
+
break;
|
|
45006
|
+
case "deny": deny.push(entry);
|
|
45007
|
+
}
|
|
45008
|
+
};
|
|
43997
45009
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
43998
45010
|
const scope = toDevinScope(category);
|
|
43999
|
-
|
|
44000
|
-
const
|
|
44001
|
-
|
|
44002
|
-
|
|
44003
|
-
|
|
44004
|
-
|
|
44005
|
-
|
|
44006
|
-
|
|
44007
|
-
|
|
44008
|
-
case "deny": deny.push(entry);
|
|
44009
|
-
}
|
|
45011
|
+
if (DEVIN_BARE_ONLY_SCOPES.has(scope)) {
|
|
45012
|
+
const action = collapseBareOnlyScopeRules({
|
|
45013
|
+
category,
|
|
45014
|
+
scope,
|
|
45015
|
+
rules,
|
|
45016
|
+
logger
|
|
45017
|
+
});
|
|
45018
|
+
if (action !== void 0) push(scope, action);
|
|
45019
|
+
continue;
|
|
44010
45020
|
}
|
|
45021
|
+
for (const [pattern, action] of Object.entries(rules)) push(buildDevinPermissionEntry(scope, pattern), action);
|
|
44011
45022
|
}
|
|
44012
45023
|
return {
|
|
44013
45024
|
allow,
|
|
@@ -46253,20 +47264,14 @@ const OPENCODE_ACTION_ONLY_PERMISSION_KEYS = /* @__PURE__ */ new Set([
|
|
|
46253
47264
|
"question",
|
|
46254
47265
|
"doom_loop"
|
|
46255
47266
|
]);
|
|
46256
|
-
const PERMISSION_ACTION_PRIORITY = {
|
|
46257
|
-
allow: 0,
|
|
46258
|
-
ask: 1,
|
|
46259
|
-
deny: 2
|
|
46260
|
-
};
|
|
46261
47267
|
function toOpencodePermission({ category, value, logger }) {
|
|
46262
47268
|
if (typeof value === "string" || !OPENCODE_ACTION_ONLY_PERMISSION_KEYS.has(category)) return value;
|
|
46263
|
-
const
|
|
46264
|
-
if (
|
|
47269
|
+
const action = collapseRulesToSingleAction({ rules: value });
|
|
47270
|
+
if (action === void 0) {
|
|
46265
47271
|
logger?.warn(`OpenCode's "${category}" permission accepts only a single action. Collapsed its empty pattern map to "deny" to avoid falling back to OpenCode's default allow behavior.`);
|
|
46266
47272
|
return "deny";
|
|
46267
47273
|
}
|
|
46268
|
-
|
|
46269
|
-
if (Object.keys(value).some((pattern) => pattern !== "*")) logger?.warn(`OpenCode's "${category}" permission accepts only a single action. Collapsed its pattern rules to "${action}" using deny > ask > allow precedence.`);
|
|
47274
|
+
if (hasPatternSpecificRules(value)) logger?.warn(`OpenCode's "${category}" permission accepts only a single action. Collapsed its pattern rules to "${action}" using deny > ask > allow precedence.`);
|
|
46270
47275
|
return action;
|
|
46271
47276
|
}
|
|
46272
47277
|
/**
|
|
@@ -46644,6 +47649,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
46644
47649
|
"visible",
|
|
46645
47650
|
"eager",
|
|
46646
47651
|
"listDirectory",
|
|
47652
|
+
"todoWrite",
|
|
46647
47653
|
"workflowsEnabled"
|
|
46648
47654
|
];
|
|
46649
47655
|
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
@@ -46749,6 +47755,11 @@ const QWEN_OVERRIDE_GROUPS = [{
|
|
|
46749
47755
|
rule: "global-machine-wide",
|
|
46750
47756
|
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
|
|
46751
47757
|
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."
|
|
47758
|
+
},
|
|
47759
|
+
todoWrite: {
|
|
47760
|
+
rule: "global-machine-wide",
|
|
47761
|
+
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.`,
|
|
47762
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
|
|
46752
47763
|
}
|
|
46753
47764
|
}
|
|
46754
47765
|
}, {
|
|
@@ -48555,9 +49566,25 @@ function widenToPrefixGlob(pattern) {
|
|
|
48555
49566
|
function toShellEntry(prefix) {
|
|
48556
49567
|
return prefix === "" ? SHELL_TOOL_NAME : `${SHELL_TOOL_NAME}(${prefix})`;
|
|
48557
49568
|
}
|
|
48558
|
-
/**
|
|
49569
|
+
/**
|
|
49570
|
+
* The `bash` pattern a `run_shell_command(<prefix>)` entry stands for. A
|
|
49571
|
+
* prefix Tabnine matches literally, so one an author spelled glob-like
|
|
49572
|
+
* (`git *`, `*`) is read as the glob it looks like rather than doubled into
|
|
49573
|
+
* `git * *`, which no comparison or warning could make sense of.
|
|
49574
|
+
*/
|
|
48559
49575
|
function toShellPattern(prefix) {
|
|
48560
|
-
|
|
49576
|
+
if (prefix === void 0 || prefix === "" || prefix === "*") return "*";
|
|
49577
|
+
return prefix.endsWith(" *") ? prefix : `${prefix} *`;
|
|
49578
|
+
}
|
|
49579
|
+
/**
|
|
49580
|
+
* Whether {@link toShellPattern} read the prefix as a glob: such an entry
|
|
49581
|
+
* would never fire on Tabnine as spelled (the prefix is matched literally, so
|
|
49582
|
+
* `run_shell_command(git *)` only matches a command line starting with the
|
|
49583
|
+
* two characters `git *`), and rulesync writes it back as the prefix the glob
|
|
49584
|
+
* denotes — a wider entry that the author is told about.
|
|
49585
|
+
*/
|
|
49586
|
+
function isGlobSpelledPrefix(prefix) {
|
|
49587
|
+
return prefix !== void 0 && (prefix === "*" || prefix.endsWith(" *"));
|
|
48561
49588
|
}
|
|
48562
49589
|
/**
|
|
48563
49590
|
* Split a `tools.allowed`/`tools.exclude` entry into its tool name and the
|
|
@@ -48683,7 +49710,7 @@ function collectTrustAffectingOverrideEntries({ tools, general }) {
|
|
|
48683
49710
|
const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
|
|
48684
49711
|
if (allowed.length > 0) entries.push({
|
|
48685
49712
|
label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
|
|
48686
|
-
reason: "auto-approves what it names as the override spells it; a non-shell entry
|
|
49713
|
+
reason: "auto-approves what it names as the override spells it; a non-shell entry is only checked against a whole-tool deny of its tool"
|
|
48687
49714
|
});
|
|
48688
49715
|
if (Object.hasOwn(tools, SANDBOX_KEY) && isNotTrue(tools[SANDBOX_KEY])) entries.push({
|
|
48689
49716
|
label: `tools.sandbox = ${quoteValueForWarning(tools[SANDBOX_KEY])}`,
|
|
@@ -48794,6 +49821,54 @@ function buildToolLists({ permission, overrideShellAllowPatterns, logger }) {
|
|
|
48794
49821
|
};
|
|
48795
49822
|
}
|
|
48796
49823
|
/**
|
|
49824
|
+
* Names the glob-spelled `run_shell_command(...)` override entries that were
|
|
49825
|
+
* written as the prefix their glob denotes. Said only for the entries that
|
|
49826
|
+
* made it into the list: one that a deny or ask overlaps is announced as
|
|
49827
|
+
* withheld by the comparison instead, and one whose glob names no prefix at
|
|
49828
|
+
* all is skipped by it.
|
|
49829
|
+
*/
|
|
49830
|
+
function warnAboutGlobSpelledOverrideEntries({ entries, allowed, logger }) {
|
|
49831
|
+
for (const { entry, pattern } of entries) {
|
|
49832
|
+
const prefix = toShellPrefix(pattern);
|
|
49833
|
+
const written = prefix === void 0 ? void 0 : toShellEntry(prefix);
|
|
49834
|
+
if (written === void 0 || !allowed.includes(written)) continue;
|
|
49835
|
+
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.`);
|
|
49836
|
+
}
|
|
49837
|
+
}
|
|
49838
|
+
/**
|
|
49839
|
+
* A verbatim override allow naming a tool the canonical block denies is
|
|
49840
|
+
* withheld the way a shadowed `bash` allow is: Tabnine never loads an excluded
|
|
49841
|
+
* tool whatever `allowed` says, so the entry would only ever contradict the
|
|
49842
|
+
* deny it sits beside — and an override is not a way around a canonical rule.
|
|
49843
|
+
* Returns the entries that may be written.
|
|
49844
|
+
*/
|
|
49845
|
+
function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logger }) {
|
|
49846
|
+
const denied = overrideVerbatimAllowed.filter((entry) => {
|
|
49847
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
49848
|
+
return toolName !== void 0 && exclude.includes(toolName);
|
|
49849
|
+
});
|
|
49850
|
+
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.`);
|
|
49851
|
+
return overrideVerbatimAllowed.filter((entry) => !denied.includes(entry));
|
|
49852
|
+
}
|
|
49853
|
+
/**
|
|
49854
|
+
* The entries of a managed tool that the canonical block did not re-derive are
|
|
49855
|
+
* named rather than removed silently. Dropping an exclude loosens the policy,
|
|
49856
|
+
* so it is a warning; dropping an allow only brings back Tabnine's confirmation
|
|
49857
|
+
* prompt, so it is said at info level for the author who wonders where a
|
|
49858
|
+
* hand-written entry went.
|
|
49859
|
+
*/
|
|
49860
|
+
function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
|
|
49861
|
+
const dropped = (key, list) => uniq(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
|
|
49862
|
+
if (list.includes(entry)) return false;
|
|
49863
|
+
const toolName = parseTabnineEntry(entry)?.toolName;
|
|
49864
|
+
return toolName === void 0 || !list.includes(toolName);
|
|
49865
|
+
});
|
|
49866
|
+
const droppedExclude = dropped(EXCLUDE_KEY, excludeList);
|
|
49867
|
+
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.`);
|
|
49868
|
+
const droppedAllowed = dropped(ALLOWED_KEY, allowedList);
|
|
49869
|
+
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.`);
|
|
49870
|
+
}
|
|
49871
|
+
/**
|
|
48797
49872
|
* Permissions generator for the Tabnine CLI.
|
|
48798
49873
|
*
|
|
48799
49874
|
* Tabnine CLI keeps its tool policy in `.tabnine/agent/settings.json` (project)
|
|
@@ -48859,7 +49934,10 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48859
49934
|
const config = rulesyncPermissions.getJson();
|
|
48860
49935
|
const override = isRecord$1(config.tabnine) ? config.tabnine : {};
|
|
48861
49936
|
const { tools: overrideTools, general: overrideGeneral, refused: refusedPaths } = stripRefusedOverridePaths(override);
|
|
48862
|
-
if (refusedPaths.length > 0)
|
|
49937
|
+
if (refusedPaths.length > 0) {
|
|
49938
|
+
const one = refusedPaths.length === 1;
|
|
49939
|
+
warnWithFallback(logger, `Tabnine CLI permissions: refused to write ${refusedPaths.join(", ")} from the tabnine override; Tabnine CLI runs ${one ? "that value" : "those values"} as a command or sends its traffic there, and a permissions file (one that came from 'rulesync fetch' included) must not be able to point it at an executable or a server of its choosing. Set ${one ? "it" : "them"} by hand in ${join(paths.relativeDirPath, paths.relativeFilePath)} if you need ${one ? "it" : "them"}.`);
|
|
49940
|
+
}
|
|
48863
49941
|
const overrideList = (key) => {
|
|
48864
49942
|
const value = overrideTools[key];
|
|
48865
49943
|
if (value === void 0 || isStringArray$2(value)) return value ?? [];
|
|
@@ -48868,10 +49946,17 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48868
49946
|
};
|
|
48869
49947
|
const overrideShellAllowPatterns = [];
|
|
48870
49948
|
const overrideVerbatimAllowed = [];
|
|
49949
|
+
const globSpelledOverrideEntries = [];
|
|
48871
49950
|
for (const entry of overrideList(ALLOWED_KEY)) {
|
|
48872
49951
|
const parsed = parseTabnineEntry(entry);
|
|
48873
|
-
if (parsed?.toolName === SHELL_TOOL_NAME)
|
|
48874
|
-
|
|
49952
|
+
if (parsed?.toolName === SHELL_TOOL_NAME) {
|
|
49953
|
+
const pattern = toShellPattern(parsed.prefix);
|
|
49954
|
+
if (isGlobSpelledPrefix(parsed.prefix)) globSpelledOverrideEntries.push({
|
|
49955
|
+
entry,
|
|
49956
|
+
pattern
|
|
49957
|
+
});
|
|
49958
|
+
overrideShellAllowPatterns.push(pattern);
|
|
49959
|
+
} else overrideVerbatimAllowed.push(entry);
|
|
48875
49960
|
}
|
|
48876
49961
|
const overrideExclude = overrideList(EXCLUDE_KEY);
|
|
48877
49962
|
const { allowed, exclude } = buildToolLists({
|
|
@@ -48879,6 +49964,16 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48879
49964
|
overrideShellAllowPatterns,
|
|
48880
49965
|
logger
|
|
48881
49966
|
});
|
|
49967
|
+
warnAboutGlobSpelledOverrideEntries({
|
|
49968
|
+
entries: globSpelledOverrideEntries,
|
|
49969
|
+
allowed,
|
|
49970
|
+
logger
|
|
49971
|
+
});
|
|
49972
|
+
const writableOverrideAllowed = withholdDeniedOverrideAllowed({
|
|
49973
|
+
overrideVerbatimAllowed,
|
|
49974
|
+
exclude,
|
|
49975
|
+
logger
|
|
49976
|
+
});
|
|
48882
49977
|
const ignoredOverrideKeys = Object.keys(override).filter((key) => key !== TOOLS_KEY && key !== GENERAL_KEY);
|
|
48883
49978
|
if (ignoredOverrideKeys.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: ignored ${ignoredOverrideKeys.length} key(s) of the tabnine override (${ignoredOverrideKeys.map(quoteValueForWarning).join(", ")}); only 'tools' and 'general' are written through the permissions feature.`);
|
|
48884
49979
|
const existingTools = this.existingToolsGroup(existingContent);
|
|
@@ -48889,7 +49984,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48889
49984
|
});
|
|
48890
49985
|
const allowedList = uniq([
|
|
48891
49986
|
...allowed,
|
|
48892
|
-
...
|
|
49987
|
+
...writableOverrideAllowed,
|
|
48893
49988
|
...preservedEntries(ALLOWED_KEY)
|
|
48894
49989
|
]);
|
|
48895
49990
|
const excludeList = uniq([
|
|
@@ -48897,12 +49992,12 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48897
49992
|
...overrideExclude,
|
|
48898
49993
|
...preservedEntries(EXCLUDE_KEY)
|
|
48899
49994
|
]);
|
|
48900
|
-
|
|
48901
|
-
|
|
48902
|
-
|
|
48903
|
-
|
|
49995
|
+
reportDroppedManagedEntries({
|
|
49996
|
+
existingTools,
|
|
49997
|
+
allowedList,
|
|
49998
|
+
excludeList,
|
|
49999
|
+
logger
|
|
48904
50000
|
});
|
|
48905
|
-
if (droppedExcludeEntries.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: removed ${droppedExcludeEntries.length} existing tools.exclude entry(ies) (${droppedExcludeEntries.map(quoteValueForWarning).join(", ")}) of a tool the canonical block manages; add a deny rule to .rulesync/permissions.jsonc to keep them.`);
|
|
48906
50001
|
const tools = {
|
|
48907
50002
|
...overrideTools,
|
|
48908
50003
|
[ALLOWED_KEY]: allowedList.length > 0 ? allowedList : void 0,
|
|
@@ -48916,7 +50011,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48916
50011
|
entries: collectTrustAffectingOverrideEntries({
|
|
48917
50012
|
tools: {
|
|
48918
50013
|
...overrideTools,
|
|
48919
|
-
[ALLOWED_KEY]:
|
|
50014
|
+
[ALLOWED_KEY]: writableOverrideAllowed
|
|
48920
50015
|
},
|
|
48921
50016
|
general: overrideGeneral
|
|
48922
50017
|
}),
|
|
@@ -52273,6 +53368,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52273
53368
|
supportsImport: true
|
|
52274
53369
|
}
|
|
52275
53370
|
}],
|
|
53371
|
+
["crush", {
|
|
53372
|
+
class: CrushPermissions,
|
|
53373
|
+
meta: {
|
|
53374
|
+
supportsProject: true,
|
|
53375
|
+
supportsGlobal: true,
|
|
53376
|
+
supportsImport: true
|
|
53377
|
+
}
|
|
53378
|
+
}],
|
|
52276
53379
|
["cursor", {
|
|
52277
53380
|
class: CursorPermissions,
|
|
52278
53381
|
meta: {
|
|
@@ -58294,6 +59397,183 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
58294
59397
|
}
|
|
58295
59398
|
};
|
|
58296
59399
|
//#endregion
|
|
59400
|
+
//#region src/constants/pool-paths.ts
|
|
59401
|
+
/**
|
|
59402
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
59403
|
+
*
|
|
59404
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
59405
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
59406
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
59407
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
59408
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
59409
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
59410
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
59411
|
+
*
|
|
59412
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
59413
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
59414
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
59415
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
59416
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
59417
|
+
*
|
|
59418
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
59419
|
+
* @see https://docs.poolside.ai/skills
|
|
59420
|
+
* @see https://github.com/poolsideai/pool
|
|
59421
|
+
*/
|
|
59422
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
59423
|
+
const POOL_DIR = ".poolside";
|
|
59424
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
59425
|
+
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
59426
|
+
/** Project skills root, relative to the project root. */
|
|
59427
|
+
const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
|
|
59428
|
+
/** Global skills root, relative to the home directory. */
|
|
59429
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
|
|
59430
|
+
//#endregion
|
|
59431
|
+
//#region src/features/skills/pool-skill.ts
|
|
59432
|
+
const PoolSkillFrontmatterSchema = z.looseObject({
|
|
59433
|
+
name: z.string(),
|
|
59434
|
+
description: z.string()
|
|
59435
|
+
});
|
|
59436
|
+
/**
|
|
59437
|
+
* Represents a Pool (Poolside) skill directory.
|
|
59438
|
+
*
|
|
59439
|
+
* Pool discovers Agent Skills (`<name>/SKILL.md` bundles, optionally with
|
|
59440
|
+
* supporting files) from `.poolside/skills/` (project) and
|
|
59441
|
+
* `~/.config/poolside/skills/` (global). It also scans the shared
|
|
59442
|
+
* `.agents/skills/` / `~/.agents/skills/` roots and the skill directories of
|
|
59443
|
+
* other Agent Skills tools; those belong to their own targets, so this class
|
|
59444
|
+
* writes only the two Pool-specific roots and a skill is written exactly once.
|
|
59445
|
+
* Pool requires the directory name to equal the frontmatter `name`, otherwise
|
|
59446
|
+
* it skips the skill. Unlike adapters that reject such a skill outright, this
|
|
59447
|
+
* one warns and still writes it: the directory name is the canonical identity
|
|
59448
|
+
* shared with every other target, and Pool merely skips the skill rather than
|
|
59449
|
+
* failing, so a hard error here would block the other targets for a
|
|
59450
|
+
* Pool-only concern. Import is lenient for the same reason.
|
|
59451
|
+
* @see https://docs.poolside.ai/skills
|
|
59452
|
+
*/
|
|
59453
|
+
var PoolSkill = class PoolSkill extends ToolSkill {
|
|
59454
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = POOL_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
59455
|
+
super({
|
|
59456
|
+
outputRoot,
|
|
59457
|
+
relativeDirPath,
|
|
59458
|
+
dirName,
|
|
59459
|
+
mainFile: {
|
|
59460
|
+
name: SKILL_FILE_NAME,
|
|
59461
|
+
body,
|
|
59462
|
+
frontmatter: { ...frontmatter }
|
|
59463
|
+
},
|
|
59464
|
+
otherFiles,
|
|
59465
|
+
global
|
|
59466
|
+
});
|
|
59467
|
+
if (validate) {
|
|
59468
|
+
const result = this.validate();
|
|
59469
|
+
if (!result.success) throw result.error;
|
|
59470
|
+
}
|
|
59471
|
+
}
|
|
59472
|
+
static getSettablePaths({ global = false } = {}) {
|
|
59473
|
+
return { relativeDirPath: global ? POOL_GLOBAL_SKILLS_DIR_PATH : POOL_SKILLS_DIR_PATH };
|
|
59474
|
+
}
|
|
59475
|
+
getFrontmatter() {
|
|
59476
|
+
return PoolSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
59477
|
+
}
|
|
59478
|
+
getBody() {
|
|
59479
|
+
return this.mainFile?.body ?? "";
|
|
59480
|
+
}
|
|
59481
|
+
validate() {
|
|
59482
|
+
if (!this.mainFile) return {
|
|
59483
|
+
success: false,
|
|
59484
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
59485
|
+
};
|
|
59486
|
+
const result = PoolSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
59487
|
+
if (!result.success) return {
|
|
59488
|
+
success: false,
|
|
59489
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
59490
|
+
};
|
|
59491
|
+
return {
|
|
59492
|
+
success: true,
|
|
59493
|
+
error: null
|
|
59494
|
+
};
|
|
59495
|
+
}
|
|
59496
|
+
toRulesyncSkill() {
|
|
59497
|
+
const frontmatter = this.getFrontmatter();
|
|
59498
|
+
const rulesyncFrontmatter = {
|
|
59499
|
+
name: frontmatter.name,
|
|
59500
|
+
description: frontmatter.description,
|
|
59501
|
+
targets: ["*"]
|
|
59502
|
+
};
|
|
59503
|
+
return new RulesyncSkill({
|
|
59504
|
+
outputRoot: this.outputRoot,
|
|
59505
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
59506
|
+
dirName: this.getDirName(),
|
|
59507
|
+
frontmatter: rulesyncFrontmatter,
|
|
59508
|
+
body: this.getBody(),
|
|
59509
|
+
otherFiles: this.getOtherFiles(),
|
|
59510
|
+
validate: true,
|
|
59511
|
+
global: this.global
|
|
59512
|
+
});
|
|
59513
|
+
}
|
|
59514
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
59515
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
59516
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
59517
|
+
const dirName = rulesyncSkill.getDirName();
|
|
59518
|
+
const poolFrontmatter = {
|
|
59519
|
+
name: rulesyncFrontmatter.name,
|
|
59520
|
+
description: rulesyncFrontmatter.description
|
|
59521
|
+
};
|
|
59522
|
+
if (poolFrontmatter.name !== dirName) warnWithFallback(logger, `${stripControlCharacters(toPosixPath(join(outputRoot, settablePaths.relativeDirPath, dirName, SKILL_FILE_NAME)))}: \`name\` ${quoteForLog(poolFrontmatter.name)} does not match its directory name ${quoteForLog(dirName)}; Pool only loads a skill whose directory name equals its \`name\``);
|
|
59523
|
+
return new PoolSkill({
|
|
59524
|
+
outputRoot,
|
|
59525
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
59526
|
+
dirName,
|
|
59527
|
+
frontmatter: poolFrontmatter,
|
|
59528
|
+
body: rulesyncSkill.getBody(),
|
|
59529
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
59530
|
+
validate,
|
|
59531
|
+
global
|
|
59532
|
+
});
|
|
59533
|
+
}
|
|
59534
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
59535
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
59536
|
+
return targets.includes("*") || targets.includes("pool");
|
|
59537
|
+
}
|
|
59538
|
+
static async fromDir(params) {
|
|
59539
|
+
const loaded = await this.loadSkillDirContent({
|
|
59540
|
+
...params,
|
|
59541
|
+
getSettablePaths: PoolSkill.getSettablePaths
|
|
59542
|
+
});
|
|
59543
|
+
const result = PoolSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
59544
|
+
if (!result.success) {
|
|
59545
|
+
const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
59546
|
+
throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
59547
|
+
}
|
|
59548
|
+
return new PoolSkill({
|
|
59549
|
+
outputRoot: loaded.outputRoot,
|
|
59550
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
59551
|
+
dirName: loaded.dirName,
|
|
59552
|
+
frontmatter: result.data,
|
|
59553
|
+
body: loaded.body,
|
|
59554
|
+
otherFiles: loaded.otherFiles,
|
|
59555
|
+
validate: true,
|
|
59556
|
+
global: loaded.global
|
|
59557
|
+
});
|
|
59558
|
+
}
|
|
59559
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
59560
|
+
const settablePaths = PoolSkill.getSettablePaths({ global });
|
|
59561
|
+
return new PoolSkill({
|
|
59562
|
+
outputRoot,
|
|
59563
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
59564
|
+
dirName,
|
|
59565
|
+
frontmatter: {
|
|
59566
|
+
name: "",
|
|
59567
|
+
description: ""
|
|
59568
|
+
},
|
|
59569
|
+
body: "",
|
|
59570
|
+
otherFiles: [],
|
|
59571
|
+
validate: false,
|
|
59572
|
+
global
|
|
59573
|
+
});
|
|
59574
|
+
}
|
|
59575
|
+
};
|
|
59576
|
+
//#endregion
|
|
58297
59577
|
//#region src/features/skills/qwencode-skill.ts
|
|
58298
59578
|
const QwencodeSkillFrontmatterSchema = z.looseObject({
|
|
58299
59579
|
name: z.string(),
|
|
@@ -60180,6 +61460,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60180
61460
|
supportsGlobal: true
|
|
60181
61461
|
}
|
|
60182
61462
|
}],
|
|
61463
|
+
["pool", {
|
|
61464
|
+
class: PoolSkill,
|
|
61465
|
+
meta: {
|
|
61466
|
+
supportsProject: true,
|
|
61467
|
+
supportsSimulated: false,
|
|
61468
|
+
supportsGlobal: true
|
|
61469
|
+
}
|
|
61470
|
+
}],
|
|
60183
61471
|
["qwencode", {
|
|
60184
61472
|
class: QwencodeSkill,
|
|
60185
61473
|
meta: {
|
|
@@ -65014,7 +66302,7 @@ var RooSubagent = class extends ToolSubagent {
|
|
|
65014
66302
|
//#region src/features/subagents/tabnine-subagent.ts
|
|
65015
66303
|
const TabnineSubagentFrontmatterSchema = z.looseObject({
|
|
65016
66304
|
name: z.string(),
|
|
65017
|
-
description: z.
|
|
66305
|
+
description: z.string(),
|
|
65018
66306
|
/** "local" (default) runs inside the CLI; "remote" delegates to a Tabnine cloud agent. */
|
|
65019
66307
|
kind: z.optional(z.string()),
|
|
65020
66308
|
/** Built-in tool names the subagent may use; omitted = every tool. */
|
|
@@ -65073,14 +66361,23 @@ var TabnineSubagent = class TabnineSubagent extends ToolSubagent {
|
|
|
65073
66361
|
validate: true
|
|
65074
66362
|
});
|
|
65075
66363
|
}
|
|
65076
|
-
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
66364
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
65077
66365
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
65078
66366
|
const tabnineSection = rulesyncFrontmatter.tabnine ?? {};
|
|
65079
|
-
const
|
|
66367
|
+
const merged = {
|
|
65080
66368
|
name: rulesyncFrontmatter.name,
|
|
65081
66369
|
description: rulesyncFrontmatter.description,
|
|
65082
66370
|
...tabnineSection
|
|
65083
66371
|
};
|
|
66372
|
+
let description = merged.description;
|
|
66373
|
+
if (description === void 0 || description === "") {
|
|
66374
|
+
description = rulesyncFrontmatter.name ? `${rulesyncFrontmatter.name} subagent` : "subagent";
|
|
66375
|
+
logger?.warn(`Tabnine CLI subagent ${rulesyncSubagent.getRelativeFilePath()} has no description, which Tabnine requires; wrote ${JSON.stringify(description)} as a placeholder.`);
|
|
66376
|
+
}
|
|
66377
|
+
const tabnineSubagentFrontmatter = {
|
|
66378
|
+
...merged,
|
|
66379
|
+
description
|
|
66380
|
+
};
|
|
65084
66381
|
const body = rulesyncSubagent.getBody();
|
|
65085
66382
|
const fileContent = stringifyFrontmatter(body, tabnineSubagentFrontmatter, { avoidBlockScalars: true });
|
|
65086
66383
|
const paths = this.getSettablePaths({ global });
|
|
@@ -71343,24 +72640,6 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
71343
72640
|
}
|
|
71344
72641
|
};
|
|
71345
72642
|
//#endregion
|
|
71346
|
-
//#region src/constants/pool-paths.ts
|
|
71347
|
-
/**
|
|
71348
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
71349
|
-
*
|
|
71350
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
71351
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
71352
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
71353
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
71354
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
71355
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
71356
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
71357
|
-
*
|
|
71358
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
71359
|
-
* @see https://github.com/poolsideai/pool
|
|
71360
|
-
*/
|
|
71361
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
71362
|
-
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
71363
|
-
//#endregion
|
|
71364
72643
|
//#region src/features/rules/pool-rule.ts
|
|
71365
72644
|
var PoolRule = class PoolRule extends ToolRule {
|
|
71366
72645
|
static getSettablePaths({ global = false } = {}) {
|
|
@@ -76427,6 +77706,6 @@ async function importChecksCore(params) {
|
|
|
76427
77706
|
return writtenCount;
|
|
76428
77707
|
}
|
|
76429
77708
|
//#endregion
|
|
76430
|
-
export { RulesyncCheck as $, writeFileContent as $t, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SKILLS_RELATIVE_DIR_PATH as An, directoryExists as At, RulesyncSkill as B, truncateText as Bn, listSubdirectoryNames as Bt, ChecksProcessor as C, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Cn, ErrorCodes as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_PERMISSIONS_SCHEMA_URL as Dn, assertWritablePathInsideRoot as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as En, assertTreeContainsNoSymlinks as Et, AUGMENTCODE_DIR as F, parseCommaSeparatedList as Fn, isFileNotFoundError as Ft, RulesyncMcp as G, stripControlCharactersKeepingLineFeeds as Gn, removeDirectoryStrict as Gt, RulesyncRule as H, hasEnclosingMarkOutsideKeycap as Hn, readFileContent as Ht, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as I, ALL_FEATURES as In, isFileSystemError as It, getRulesyncSourceCandidates as J, removeTempDirectory as Jt, RulesyncIgnore as K, stripHiddenCharacters as Kn, removeFile as Kt, getLocalSkillDirNames as L, ALL_FEATURES_WITH_WILDCARD as Ln, isSymlink as Lt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as M, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Mn, fileExists as Mt, caseFoldIdentity as N, RULESYNC_USER_CONFIG_DIR_NAME as Nn, getFileSize as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RELATIVE_DIR_PATH as On, checkPathTraversal as Ot, groupSpellingsByCaseFoldedIdentity as P, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Pn, getHomeDirectory as Pt, RulesyncCommandFrontmatterSchema as Q, writeFileBuffer as Qt, RulesyncSubagent as R, DEPRECATED_FEATURE_REPLACEMENTS as Rn, listDirectoryEntryNames as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_MCP_SCHEMA_URL as Sn, CLIError as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Tn, assertDirectoryIfExists as Tt, RulesyncRuleFrontmatterSchema as U, quoteForLog as Un, readFileContentOrNull as Ut, RulesyncSkillFrontmatterSchema as V, hasDeceptiveHiddenCharacters as Vn, pathEscapesRoot as Vt, RulesyncPermissions as W, stripControlCharacters as Wn, removeDirectory as Wt, parseJsonc as X, runWithDirectoryRollback as Xt, resolveRulesyncSourceWritePath as Y, resolvePath as Yt, RulesyncCommand as Z, toPosixPath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, fallbackLogger as _t, getProcessorRegistryEntry as a, MAX_FILE_SIZE as an, ConfigResolver as at, CommandsProcessor as b, RULESYNC_MCP_LEGACY_FILE_NAME as bn, resetRunWarningState as bt, RulesProcessor as c, RULESYNC_CHECKS_RELATIVE_DIR_PATH as cn, CONFLICTING_TARGET_PAIRS as ct, CODEBUDDY_DIR as d, RULESYNC_CONFIG_SCHEMA_URL as dn, SourceEntrySchema as dt, ALL_TOOL_TARGETS as en, RulesyncCheckFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as fn, assertTargetsFeaturesExclusive as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, WarningCollectingLogger as gt, shortenToWidth as h, RULESYNC_HOOKS_LEGACY_FILE_NAME as hn, JsonLogger as ht, inspectInputRoots as i, CURATED_RULES_FEATURE_SUBDIR as in, SKILL_FILE_NAME as it, FACTORYDROID_DIR as j, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as jn, ensureDir as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_RULES_RELATIVE_DIR_PATH as kn, createTempDirectory as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ln, ConfigFileSchema as lt, displayWidthOf as m, RULESYNC_HOOKS_FILE_NAME as mn, ConsoleLogger as mt, formatSourceLoadFailure as n, PACKAGING_TOOL_TARGETS as nn, loadYaml as nt, convertFromTool as o, RULESYNC_AIIGNORE_FILE_NAME as on, mergeInputRootConfigs as ot, ELLIPSIS_WIDTH as p, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as pn, findControlCharacter as pt, RulesyncHooks as q, removeFileStrict as qt, generate as r, ToolTargetSchema as rn, SHARED_USER_MANAGED_CONFIG_PATHS as rt, isPackagingToolTarget as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as sn, resolveEffectiveInputRoots as st, importFromTool as t, ALL_TOOL_TARGETS_WITH_WILDCARD as tn, stringifyFrontmatter as tt, SkillsProcessor as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as un, GITIGNORE_DESTINATION_KEY as ut,
|
|
77709
|
+
export { RulesyncCheck as $, writeFileContent as $t, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SKILLS_RELATIVE_DIR_PATH as An, directoryExists as At, RulesyncSkill as B, truncateText as Bn, listSubdirectoryNames as Bt, ChecksProcessor as C, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Cn, ErrorCodes as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_PERMISSIONS_SCHEMA_URL as Dn, assertWritablePathInsideRoot as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as En, assertTreeContainsNoSymlinks as Et, AUGMENTCODE_DIR as F, parseCommaSeparatedList as Fn, isFileNotFoundError as Ft, RulesyncMcp as G, stripControlCharactersKeepingLineFeeds as Gn, removeDirectoryStrict as Gt, RulesyncRule as H, hasEnclosingMarkOutsideKeycap as Hn, readFileContent as Ht, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as I, ALL_FEATURES as In, isFileSystemError as It, getRulesyncSourceCandidates as J, removeTempDirectory as Jt, RulesyncIgnore as K, stripHiddenCharacters as Kn, removeFile as Kt, getLocalSkillDirNames as L, ALL_FEATURES_WITH_WILDCARD as Ln, isSymlink as Lt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as M, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Mn, fileExists as Mt, caseFoldIdentity as N, RULESYNC_USER_CONFIG_DIR_NAME as Nn, getFileSize as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RELATIVE_DIR_PATH as On, checkPathTraversal as Ot, groupSpellingsByCaseFoldedIdentity as P, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Pn, getHomeDirectory as Pt, RulesyncCommandFrontmatterSchema as Q, writeFileBuffer as Qt, RulesyncSubagent as R, DEPRECATED_FEATURE_REPLACEMENTS as Rn, listDirectoryEntryNames as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_MCP_SCHEMA_URL as Sn, CLIError as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Tn, assertDirectoryIfExists as Tt, RulesyncRuleFrontmatterSchema as U, quoteForLog as Un, readFileContentOrNull as Ut, RulesyncSkillFrontmatterSchema as V, hasDeceptiveHiddenCharacters as Vn, pathEscapesRoot as Vt, RulesyncPermissions as W, stripControlCharacters as Wn, removeDirectory as Wt, parseJsonc as X, runWithDirectoryRollback as Xt, resolveRulesyncSourceWritePath as Y, resolvePath as Yt, RulesyncCommand as Z, toPosixPath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, fallbackLogger as _t, getProcessorRegistryEntry as a, MAX_FILE_SIZE as an, ConfigResolver as at, CommandsProcessor as b, RULESYNC_MCP_LEGACY_FILE_NAME as bn, resetRunWarningState as bt, RulesProcessor as c, RULESYNC_CHECKS_RELATIVE_DIR_PATH as cn, CONFLICTING_TARGET_PAIRS as ct, CODEBUDDY_DIR as d, RULESYNC_CONFIG_SCHEMA_URL as dn, SourceEntrySchema as dt, ALL_TOOL_TARGETS as en, RulesyncCheckFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as fn, assertTargetsFeaturesExclusive as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, WarningCollectingLogger as gt, shortenToWidth as h, RULESYNC_HOOKS_LEGACY_FILE_NAME as hn, JsonLogger as ht, inspectInputRoots as i, CURATED_RULES_FEATURE_SUBDIR as in, SKILL_FILE_NAME as it, FACTORYDROID_DIR as j, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as jn, ensureDir as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_RULES_RELATIVE_DIR_PATH as kn, createTempDirectory as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ln, ConfigFileSchema as lt, displayWidthOf as m, RULESYNC_HOOKS_FILE_NAME as mn, ConsoleLogger as mt, formatSourceLoadFailure as n, PACKAGING_TOOL_TARGETS as nn, loadYaml as nt, convertFromTool as o, RULESYNC_AIIGNORE_FILE_NAME as on, mergeInputRootConfigs as ot, ELLIPSIS_WIDTH as p, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as pn, findControlCharacter as pt, RulesyncHooks as q, removeFileStrict as qt, generate as r, ToolTargetSchema as rn, SHARED_USER_MANAGED_CONFIG_PATHS as rt, isPackagingToolTarget as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as sn, resolveEffectiveInputRoots as st, importFromTool as t, ALL_TOOL_TARGETS_WITH_WILDCARD as tn, stringifyFrontmatter as tt, SkillsProcessor as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as un, GITIGNORE_DESTINATION_KEY as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, warnOnConflictingFlags as vt, CODEXCLI_BASH_RULES_FILE_NAME as w, RULESYNC_PERMISSIONS_FILE_NAME as wn, applyFileMode as wt, QWENCODE_DIR as x, RULESYNC_MCP_RELATIVE_FILE_PATH as xn, withWarnOnceScope as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_FILE_NAME as yn, withFallbackLoggerTarget as yt, RulesyncSubagentFrontmatterSchema as z, formatError as zn, listFilePathsRecursively as zt };
|
|
76431
77710
|
|
|
76432
|
-
//# sourceMappingURL=import-
|
|
77711
|
+
//# sourceMappingURL=import-C6R-lXFF.js.map
|