rulesync 16.32.0 → 16.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli/index.cjs +4 -4
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-D5n_V39C.js → import-C6R-lXFF.js} +1856 -567
- package/dist/import-C6R-lXFF.js.map +1 -0
- package/dist/{import-0wSuIchf.cjs → import-DVRU6cLB.cjs} +1871 -582
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-D5n_V39C.js.map +0 -1
|
@@ -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.
|
|
@@ -30689,13 +30974,17 @@ function isRemoteMcpServer(serverConfig) {
|
|
|
30689
30974
|
function resolveRemoteMcpUrl(serverConfig) {
|
|
30690
30975
|
return serverConfig.url || serverConfig.httpUrl || void 0;
|
|
30691
30976
|
}
|
|
30692
|
-
/**
|
|
30977
|
+
/**
|
|
30978
|
+
* The `command` array a `local` server is spawned with, `args` merged in. An
|
|
30979
|
+
* entry carrying `args` but no `command` comes back empty rather than with
|
|
30980
|
+
* `args[0]` promoted to its program: every adapter writes element 0 back out
|
|
30981
|
+
* as the executable (or the whole array as the command line), so the caller
|
|
30982
|
+
* skips such an entry the same way as one with nothing to spawn at all.
|
|
30983
|
+
*/
|
|
30693
30984
|
function resolveLocalMcpCommand(serverConfig) {
|
|
30694
|
-
const
|
|
30695
|
-
|
|
30696
|
-
|
|
30697
|
-
else commandArray.push(serverConfig.command);
|
|
30698
|
-
}
|
|
30985
|
+
const { command } = serverConfig;
|
|
30986
|
+
const commandArray = Array.isArray(command) ? [...command] : command ? [command] : [];
|
|
30987
|
+
if (commandArray.length === 0) return [];
|
|
30699
30988
|
if (serverConfig.args) commandArray.push(...serverConfig.args);
|
|
30700
30989
|
return commandArray;
|
|
30701
30990
|
}
|
|
@@ -30705,9 +30994,13 @@ function resolveLocalMcpCommand(serverConfig) {
|
|
|
30705
30994
|
* spells the transport out. Writing `{type: "local", command: []}` for it would
|
|
30706
30995
|
* give the tool a server it cannot start and the importer a file it cannot read
|
|
30707
30996
|
* back, so it is skipped out loud instead.
|
|
30997
|
+
*
|
|
30998
|
+
* The server name comes straight from the user's config, so it is quoted
|
|
30999
|
+
* through `quoteValueForWarning` rather than interpolated: a crafted name could
|
|
31000
|
+
* otherwise carry a line break or control characters into the log.
|
|
30708
31001
|
*/
|
|
30709
31002
|
function warnAndSkipMcpServer({ toolName, serverName, reason, logger }) {
|
|
30710
|
-
logger?.warn(`${toolName} MCP: skipping
|
|
31003
|
+
logger?.warn(`${toolName} MCP: skipping ${quoteValueForWarning(serverName)} because it declares ${reason}.`);
|
|
30711
31004
|
return null;
|
|
30712
31005
|
}
|
|
30713
31006
|
/**
|
|
@@ -30795,8 +31088,10 @@ function asBobRemoteType(stated, url) {
|
|
|
30795
31088
|
* streamable HTTP server carries `type: "streamable-http"` and `url`, and an
|
|
30796
31089
|
* SSE server carries a bare `url`. The canonical `transport` alias and the
|
|
30797
31090
|
* Claude-style `httpUrl` alias are folded into `type`/`url`; `env`, `cwd`,
|
|
30798
|
-
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through
|
|
30799
|
-
*
|
|
31091
|
+
* `headers`, `timeout`, `alwaysAllow` and `disabled` pass through, as Bob
|
|
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).
|
|
30800
31095
|
*
|
|
30801
31096
|
* Bob Shell documents the same file with an `httpURL` key instead of
|
|
30802
31097
|
* `type` + `url` for streamable HTTP. rulesync writes the IDE spelling (the two
|
|
@@ -30864,7 +31159,7 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30864
31159
|
}
|
|
30865
31160
|
for (const [key, value] of Object.entries(rest)) {
|
|
30866
31161
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30867
|
-
converted[key] = (
|
|
31162
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30868
31163
|
}
|
|
30869
31164
|
result[serverName] = converted;
|
|
30870
31165
|
}
|
|
@@ -30875,7 +31170,9 @@ function convertToBobFormat(mcpServers, logger) {
|
|
|
30875
31170
|
* (`type: "streamable-http"` + `url`) is already canonical and passes through;
|
|
30876
31171
|
* the Bob Shell spelling `httpURL` becomes `url` with `type: "http"`; a bare
|
|
30877
31172
|
* `url` is an SSE server in Bob, so it gains `type: "sse"` to keep that reading
|
|
30878
|
-
* 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`.
|
|
30879
31176
|
*/
|
|
30880
31177
|
function convertFromBobFormat(mcpServers) {
|
|
30881
31178
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -30890,7 +31187,7 @@ function convertFromBobFormat(mcpServers) {
|
|
|
30890
31187
|
if (typeof value === "string") httpURL = value;
|
|
30891
31188
|
continue;
|
|
30892
31189
|
}
|
|
30893
|
-
converted[key] = value;
|
|
31190
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
30894
31191
|
}
|
|
30895
31192
|
if (httpURL !== void 0) {
|
|
30896
31193
|
converted.url = httpURL;
|
|
@@ -31738,7 +32035,7 @@ function convertRemoteServer({ serverName, serverConfig, logger }) {
|
|
|
31738
32035
|
return converted;
|
|
31739
32036
|
}
|
|
31740
32037
|
function convertStdioServer({ serverName, serverConfig, logger }) {
|
|
31741
|
-
const [command, ...args] =
|
|
32038
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
31742
32039
|
if (!command) {
|
|
31743
32040
|
warnAndSkipMcpServer({
|
|
31744
32041
|
toolName: "Continue",
|
|
@@ -31754,7 +32051,7 @@ function convertStdioServer({ serverName, serverConfig, logger }) {
|
|
|
31754
32051
|
};
|
|
31755
32052
|
if (args.length > 0) converted.args = args;
|
|
31756
32053
|
if (isRecord$1(serverConfig.env)) converted.env = omitPrototypePollutionKeys(serverConfig.env);
|
|
31757
|
-
if (typeof serverConfig.envFile === "string") logger?.warn(`Continue ignores "envFile" for MCP servers, so the envFile of server
|
|
32054
|
+
if (typeof serverConfig.envFile === "string") logger?.warn(`Continue ignores "envFile" for MCP servers, so the envFile of server ${quoteValueForWarning(serverName)} was not written; put the variables in "env" instead.`);
|
|
31758
32055
|
return converted;
|
|
31759
32056
|
}
|
|
31760
32057
|
/**
|
|
@@ -32296,7 +32593,7 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32296
32593
|
}
|
|
32297
32594
|
for (const [key, value] of Object.entries(rest)) {
|
|
32298
32595
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
32299
|
-
converted[key] = (
|
|
32596
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
32300
32597
|
}
|
|
32301
32598
|
result[serverName] = converted;
|
|
32302
32599
|
}
|
|
@@ -32305,14 +32602,15 @@ function convertToCortexcodeFormat(mcpServers, logger) {
|
|
|
32305
32602
|
/**
|
|
32306
32603
|
* Convert Cortex Code's server map back to the canonical shape. The
|
|
32307
32604
|
* documented spelling (`type` + `command`/`url`) is already canonical, so
|
|
32308
|
-
* 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.
|
|
32309
32607
|
*/
|
|
32310
32608
|
function convertFromCortexcodeFormat(mcpServers) {
|
|
32311
32609
|
if (!isMcpServers(mcpServers)) return {};
|
|
32312
32610
|
const result = {};
|
|
32313
32611
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
32314
32612
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
32315
|
-
result[serverName] =
|
|
32613
|
+
result[serverName] = omitPrototypePollutionKeysDeep(serverConfig);
|
|
32316
32614
|
}
|
|
32317
32615
|
return result;
|
|
32318
32616
|
}
|
|
@@ -32351,19 +32649,12 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32351
32649
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32352
32650
|
if (!global) throw new Error(CORTEXCODE_GLOBAL_ONLY_MESSAGE);
|
|
32353
32651
|
const paths = this.getSettablePaths({ global });
|
|
32354
|
-
const
|
|
32355
|
-
fileContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}",
|
|
32356
|
-
relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
|
|
32357
|
-
});
|
|
32358
|
-
const newJson = {
|
|
32359
|
-
...json,
|
|
32360
|
-
mcpServers: json.mcpServers ?? {}
|
|
32361
|
-
};
|
|
32652
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
32362
32653
|
return new CortexcodeMcp({
|
|
32363
32654
|
outputRoot,
|
|
32364
32655
|
relativeDirPath: paths.relativeDirPath,
|
|
32365
32656
|
relativeFilePath: paths.relativeFilePath,
|
|
32366
|
-
fileContent
|
|
32657
|
+
fileContent,
|
|
32367
32658
|
validate,
|
|
32368
32659
|
global
|
|
32369
32660
|
});
|
|
@@ -32411,6 +32702,319 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
32411
32702
|
}
|
|
32412
32703
|
};
|
|
32413
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
|
|
32414
33018
|
//#region src/features/mcp/mcp-env-var-format.ts
|
|
32415
33019
|
/**
|
|
32416
33020
|
* Canonical rulesync env var reference pattern: `${VAR}` (but not `${env:VAR}`,
|
|
@@ -36455,7 +37059,7 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36455
37059
|
}
|
|
36456
37060
|
for (const [key, value] of Object.entries(rest)) {
|
|
36457
37061
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36458
|
-
converted[key] = (
|
|
37062
|
+
converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36459
37063
|
}
|
|
36460
37064
|
if (enabledTools !== void 0) converted.includeTools = enabledTools;
|
|
36461
37065
|
if (disabledTools !== void 0) converted.excludeTools = disabledTools;
|
|
@@ -36466,7 +37070,9 @@ function convertToTabnineFormat(mcpServers, logger) {
|
|
|
36466
37070
|
/**
|
|
36467
37071
|
* Convert Tabnine's server map back to the canonical shape: `includeTools` /
|
|
36468
37072
|
* `excludeTools` become `enabledTools` / `disabledTools`; `type` (`stdio`,
|
|
36469
|
-
* `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.
|
|
36470
37076
|
*/
|
|
36471
37077
|
function convertFromTabnineFormat(mcpServers) {
|
|
36472
37078
|
if (!isMcpServers(mcpServers)) return {};
|
|
@@ -36478,7 +37084,7 @@ function convertFromTabnineFormat(mcpServers) {
|
|
|
36478
37084
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36479
37085
|
if (key === "includeTools") converted.enabledTools = value;
|
|
36480
37086
|
else if (key === "excludeTools") converted.disabledTools = value;
|
|
36481
|
-
else converted[key] = value;
|
|
37087
|
+
else converted[key] = omitPrototypePollutionKeysDeep(value);
|
|
36482
37088
|
}
|
|
36483
37089
|
result[serverName] = converted;
|
|
36484
37090
|
}
|
|
@@ -37626,6 +38232,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
37626
38232
|
supportsDisabledTools: false
|
|
37627
38233
|
}
|
|
37628
38234
|
}],
|
|
38235
|
+
["crush", {
|
|
38236
|
+
class: CrushMcp,
|
|
38237
|
+
meta: {
|
|
38238
|
+
supportsProject: true,
|
|
38239
|
+
supportsGlobal: true,
|
|
38240
|
+
supportsEnabledTools: true,
|
|
38241
|
+
supportsDisabledTools: true
|
|
38242
|
+
}
|
|
38243
|
+
}],
|
|
37629
38244
|
["cursor", {
|
|
37630
38245
|
class: CursorMcp,
|
|
37631
38246
|
meta: {
|
|
@@ -39057,7 +39672,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
39057
39672
|
function toAntigravityCliToolName(canonical) {
|
|
39058
39673
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
39059
39674
|
}
|
|
39060
|
-
function toCanonicalToolName$
|
|
39675
|
+
function toCanonicalToolName$8(cliName) {
|
|
39061
39676
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
39062
39677
|
}
|
|
39063
39678
|
/**
|
|
@@ -39273,7 +39888,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
39273
39888
|
const processEntries = (entries, action) => {
|
|
39274
39889
|
for (const entry of entries) {
|
|
39275
39890
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
39276
|
-
const canonical = toCanonicalToolName$
|
|
39891
|
+
const canonical = toCanonicalToolName$8(toolName);
|
|
39277
39892
|
if (!permission[canonical]) permission[canonical] = {};
|
|
39278
39893
|
permission[canonical][pattern] = action;
|
|
39279
39894
|
}
|
|
@@ -39530,11 +40145,34 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
|
|
|
39530
40145
|
websearch: "web-search"
|
|
39531
40146
|
};
|
|
39532
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
|
+
};
|
|
39533
40154
|
function toAugmentToolName(canonical) {
|
|
39534
|
-
return
|
|
40155
|
+
return lookupOwn({
|
|
40156
|
+
record: CANONICAL_TO_AUGMENT_TOOL_NAMES,
|
|
40157
|
+
key: canonical
|
|
40158
|
+
}) ?? canonical;
|
|
39535
40159
|
}
|
|
39536
|
-
|
|
39537
|
-
|
|
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;
|
|
40169
|
+
}
|
|
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;
|
|
39538
40176
|
}
|
|
39539
40177
|
function actionToAugmentType(action) {
|
|
39540
40178
|
switch (action) {
|
|
@@ -39750,10 +40388,11 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
39750
40388
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
39751
40389
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
39752
40390
|
if (entry.toolName === "*") return false;
|
|
39753
|
-
|
|
40391
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40392
|
+
if (!MANAGED_AUGMENT_TOOL_NAMES.has(legacyToolName)) return true;
|
|
39754
40393
|
if (entry.permission.type === "deny") {
|
|
39755
|
-
const
|
|
39756
|
-
return !generatedKeys.has(
|
|
40394
|
+
const suffix = `|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
40395
|
+
return !generatedKeys.has(`${entry.toolName}${suffix}`) && !generatedKeys.has(`${legacyToolName}${suffix}`);
|
|
39757
40396
|
}
|
|
39758
40397
|
return false;
|
|
39759
40398
|
});
|
|
@@ -39980,14 +40619,15 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
39980
40619
|
if (isSpecialEntry(entry)) continue;
|
|
39981
40620
|
const type = entry.permission.type;
|
|
39982
40621
|
if (!isBasicAugmentType(type)) continue;
|
|
39983
|
-
const
|
|
40622
|
+
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
40623
|
+
const canonical = toCanonicalToolName$7(entry.toolName);
|
|
39984
40624
|
if (forbiddenMapKeys.has(canonical)) {
|
|
39985
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.`);
|
|
39986
40626
|
continue;
|
|
39987
40627
|
}
|
|
39988
40628
|
const action = augmentTypeToAction(type);
|
|
39989
40629
|
let pattern;
|
|
39990
|
-
if (
|
|
40630
|
+
if (legacyToolName === "launch-process" && entry.shellInputRegex) {
|
|
39991
40631
|
const regex = entry.shellInputRegex;
|
|
39992
40632
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
39993
40633
|
else if (action === "deny") {
|
|
@@ -40002,9 +40642,19 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
40002
40642
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because its pattern resolves to the reserved object key '${pattern}'.`);
|
|
40003
40643
|
continue;
|
|
40004
40644
|
}
|
|
40005
|
-
|
|
40006
|
-
|
|
40007
|
-
|
|
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;
|
|
40008
40658
|
}
|
|
40009
40659
|
return { permission };
|
|
40010
40660
|
}
|
|
@@ -40215,7 +40865,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
40215
40865
|
function toClaudeToolName(canonical) {
|
|
40216
40866
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
40217
40867
|
}
|
|
40218
|
-
function toCanonicalToolName$
|
|
40868
|
+
function toCanonicalToolName$6(claudeName) {
|
|
40219
40869
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
40220
40870
|
}
|
|
40221
40871
|
/**
|
|
@@ -41110,7 +41760,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
41110
41760
|
const processEntries = (entries, action) => {
|
|
41111
41761
|
for (const entry of entries) {
|
|
41112
41762
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
41113
|
-
const canonical = toCanonicalToolName$
|
|
41763
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
41114
41764
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41115
41765
|
permission[canonical][pattern] = action;
|
|
41116
41766
|
}
|
|
@@ -41935,7 +42585,16 @@ function mapBashActionToDecision(action) {
|
|
|
41935
42585
|
//#endregion
|
|
41936
42586
|
//#region src/features/permissions/continue-permissions.ts
|
|
41937
42587
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
41938
|
-
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
|
+
}
|
|
41939
42598
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
41940
42599
|
bash: "Bash",
|
|
41941
42600
|
read: "Read",
|
|
@@ -41963,7 +42622,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
41963
42622
|
function toContinueToolName(canonical) {
|
|
41964
42623
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
41965
42624
|
}
|
|
41966
|
-
function toCanonicalToolName$
|
|
42625
|
+
function toCanonicalToolName$5(continueName) {
|
|
41967
42626
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
41968
42627
|
}
|
|
41969
42628
|
/**
|
|
@@ -41971,7 +42630,7 @@ function toCanonicalToolName$4(continueName) {
|
|
|
41971
42630
|
* `Tool(pattern)`.
|
|
41972
42631
|
*/
|
|
41973
42632
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
41974
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
42633
|
+
return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
|
|
41975
42634
|
}
|
|
41976
42635
|
/**
|
|
41977
42636
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -41985,7 +42644,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
41985
42644
|
if (toolName === "") return;
|
|
41986
42645
|
return {
|
|
41987
42646
|
toolName,
|
|
41988
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
42647
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
|
|
41989
42648
|
};
|
|
41990
42649
|
}
|
|
41991
42650
|
/**
|
|
@@ -42152,13 +42811,14 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
42152
42811
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42153
42812
|
if (isPrototypePollutionKey(category)) continue;
|
|
42154
42813
|
const toolName = category === "*" ? "*" : toContinueToolName(category);
|
|
42155
|
-
for (const [
|
|
42156
|
-
if (isPrototypePollutionKey(
|
|
42814
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
42815
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
42816
|
+
const pattern = normalizeCatchAllPattern$1(rawPattern);
|
|
42157
42817
|
if (pattern.includes("(") || pattern.includes(")")) {
|
|
42158
42818
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42159
42819
|
continue;
|
|
42160
42820
|
}
|
|
42161
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
42821
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
|
|
42162
42822
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
42163
42823
|
continue;
|
|
42164
42824
|
}
|
|
@@ -42183,7 +42843,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
42183
42843
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
42184
42844
|
if (parsedEntry === void 0) continue;
|
|
42185
42845
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
42186
|
-
const category = toCanonicalToolName$
|
|
42846
|
+
const category = toCanonicalToolName$5(parsedEntry.toolName);
|
|
42187
42847
|
permission[category] ??= {};
|
|
42188
42848
|
permission[category][parsedEntry.pattern] ??= action;
|
|
42189
42849
|
}
|
|
@@ -42506,6 +43166,295 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
42506
43166
|
}
|
|
42507
43167
|
};
|
|
42508
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
|
|
42509
43458
|
//#region src/features/permissions/cursor-permissions.ts
|
|
42510
43459
|
/**
|
|
42511
43460
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -43660,6 +44609,40 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43660
44609
|
return Object.keys(bash).length > 0 ? { permission: { bash } } : { permission: {} };
|
|
43661
44610
|
}
|
|
43662
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
|
|
43663
44646
|
//#region src/features/permissions/devin-permissions.ts
|
|
43664
44647
|
/**
|
|
43665
44648
|
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
@@ -43669,17 +44652,21 @@ function convertDeepagentsToRulesyncPermissions({ allowList }) {
|
|
|
43669
44652
|
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
43670
44653
|
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
43671
44654
|
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
43672
|
-
* `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
|
|
43673
44658
|
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
43674
44659
|
*
|
|
43675
44660
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
44661
|
+
* @see https://docs.devin.ai/cli/changelog/stable — v3000.10.21, `web_search`
|
|
43676
44662
|
*/
|
|
43677
44663
|
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
43678
44664
|
read: "Read",
|
|
43679
44665
|
write: "Write",
|
|
43680
44666
|
edit: "Write",
|
|
43681
44667
|
bash: "Exec",
|
|
43682
|
-
webfetch: "Fetch"
|
|
44668
|
+
webfetch: "Fetch",
|
|
44669
|
+
websearch: "web_search"
|
|
43683
44670
|
};
|
|
43684
44671
|
/**
|
|
43685
44672
|
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
@@ -43688,8 +44675,15 @@ const DEVIN_SCOPE_TO_CANONICAL = {
|
|
|
43688
44675
|
Read: "read",
|
|
43689
44676
|
Write: "write",
|
|
43690
44677
|
Exec: "bash",
|
|
43691
|
-
Fetch: "webfetch"
|
|
44678
|
+
Fetch: "webfetch",
|
|
44679
|
+
web_search: "websearch"
|
|
43692
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"]);
|
|
43693
44687
|
function toDevinScope(canonical) {
|
|
43694
44688
|
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
43695
44689
|
}
|
|
@@ -43874,7 +44868,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43874
44868
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
43875
44869
|
}
|
|
43876
44870
|
const config = rulesyncPermissions.getJson();
|
|
43877
|
-
const { allow, ask, deny } = convertRulesyncToDevinPermissions(
|
|
44871
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions({
|
|
44872
|
+
config,
|
|
44873
|
+
logger
|
|
44874
|
+
});
|
|
43878
44875
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
43879
44876
|
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
43880
44877
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
@@ -43978,26 +44975,50 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
43978
44975
|
}
|
|
43979
44976
|
};
|
|
43980
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
|
+
/**
|
|
43981
44992
|
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
43982
44993
|
*/
|
|
43983
|
-
function convertRulesyncToDevinPermissions(config) {
|
|
44994
|
+
function convertRulesyncToDevinPermissions({ config, logger }) {
|
|
43984
44995
|
const allow = [];
|
|
43985
44996
|
const ask = [];
|
|
43986
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
|
+
};
|
|
43987
45009
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
43988
45010
|
const scope = toDevinScope(category);
|
|
43989
|
-
|
|
43990
|
-
const
|
|
43991
|
-
|
|
43992
|
-
|
|
43993
|
-
|
|
43994
|
-
|
|
43995
|
-
|
|
43996
|
-
|
|
43997
|
-
|
|
43998
|
-
case "deny": deny.push(entry);
|
|
43999
|
-
}
|
|
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;
|
|
44000
45020
|
}
|
|
45021
|
+
for (const [pattern, action] of Object.entries(rules)) push(buildDevinPermissionEntry(scope, pattern), action);
|
|
44001
45022
|
}
|
|
44002
45023
|
return {
|
|
44003
45024
|
allow,
|
|
@@ -46243,20 +47264,14 @@ const OPENCODE_ACTION_ONLY_PERMISSION_KEYS = /* @__PURE__ */ new Set([
|
|
|
46243
47264
|
"question",
|
|
46244
47265
|
"doom_loop"
|
|
46245
47266
|
]);
|
|
46246
|
-
const PERMISSION_ACTION_PRIORITY = {
|
|
46247
|
-
allow: 0,
|
|
46248
|
-
ask: 1,
|
|
46249
|
-
deny: 2
|
|
46250
|
-
};
|
|
46251
47267
|
function toOpencodePermission({ category, value, logger }) {
|
|
46252
47268
|
if (typeof value === "string" || !OPENCODE_ACTION_ONLY_PERMISSION_KEYS.has(category)) return value;
|
|
46253
|
-
const
|
|
46254
|
-
if (
|
|
47269
|
+
const action = collapseRulesToSingleAction({ rules: value });
|
|
47270
|
+
if (action === void 0) {
|
|
46255
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.`);
|
|
46256
47272
|
return "deny";
|
|
46257
47273
|
}
|
|
46258
|
-
|
|
46259
|
-
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.`);
|
|
46260
47275
|
return action;
|
|
46261
47276
|
}
|
|
46262
47277
|
/**
|
|
@@ -46634,6 +47649,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
46634
47649
|
"visible",
|
|
46635
47650
|
"eager",
|
|
46636
47651
|
"listDirectory",
|
|
47652
|
+
"todoWrite",
|
|
46637
47653
|
"workflowsEnabled"
|
|
46638
47654
|
];
|
|
46639
47655
|
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
@@ -46739,6 +47755,11 @@ const QWEN_OVERRIDE_GROUPS = [{
|
|
|
46739
47755
|
rule: "global-machine-wide",
|
|
46740
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.`,
|
|
46741
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."
|
|
46742
47763
|
}
|
|
46743
47764
|
}
|
|
46744
47765
|
}, {
|
|
@@ -48545,9 +49566,25 @@ function widenToPrefixGlob(pattern) {
|
|
|
48545
49566
|
function toShellEntry(prefix) {
|
|
48546
49567
|
return prefix === "" ? SHELL_TOOL_NAME : `${SHELL_TOOL_NAME}(${prefix})`;
|
|
48547
49568
|
}
|
|
48548
|
-
/**
|
|
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
|
+
*/
|
|
48549
49575
|
function toShellPattern(prefix) {
|
|
48550
|
-
|
|
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(" *"));
|
|
48551
49588
|
}
|
|
48552
49589
|
/**
|
|
48553
49590
|
* Split a `tools.allowed`/`tools.exclude` entry into its tool name and the
|
|
@@ -48673,7 +49710,7 @@ function collectTrustAffectingOverrideEntries({ tools, general }) {
|
|
|
48673
49710
|
const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
|
|
48674
49711
|
if (allowed.length > 0) entries.push({
|
|
48675
49712
|
label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
|
|
48676
|
-
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"
|
|
48677
49714
|
});
|
|
48678
49715
|
if (Object.hasOwn(tools, SANDBOX_KEY) && isNotTrue(tools[SANDBOX_KEY])) entries.push({
|
|
48679
49716
|
label: `tools.sandbox = ${quoteValueForWarning(tools[SANDBOX_KEY])}`,
|
|
@@ -48784,6 +49821,54 @@ function buildToolLists({ permission, overrideShellAllowPatterns, logger }) {
|
|
|
48784
49821
|
};
|
|
48785
49822
|
}
|
|
48786
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
|
+
/**
|
|
48787
49872
|
* Permissions generator for the Tabnine CLI.
|
|
48788
49873
|
*
|
|
48789
49874
|
* Tabnine CLI keeps its tool policy in `.tabnine/agent/settings.json` (project)
|
|
@@ -48849,7 +49934,10 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48849
49934
|
const config = rulesyncPermissions.getJson();
|
|
48850
49935
|
const override = isRecord$1(config.tabnine) ? config.tabnine : {};
|
|
48851
49936
|
const { tools: overrideTools, general: overrideGeneral, refused: refusedPaths } = stripRefusedOverridePaths(override);
|
|
48852
|
-
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
|
+
}
|
|
48853
49941
|
const overrideList = (key) => {
|
|
48854
49942
|
const value = overrideTools[key];
|
|
48855
49943
|
if (value === void 0 || isStringArray$2(value)) return value ?? [];
|
|
@@ -48858,10 +49946,17 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48858
49946
|
};
|
|
48859
49947
|
const overrideShellAllowPatterns = [];
|
|
48860
49948
|
const overrideVerbatimAllowed = [];
|
|
49949
|
+
const globSpelledOverrideEntries = [];
|
|
48861
49950
|
for (const entry of overrideList(ALLOWED_KEY)) {
|
|
48862
49951
|
const parsed = parseTabnineEntry(entry);
|
|
48863
|
-
if (parsed?.toolName === SHELL_TOOL_NAME)
|
|
48864
|
-
|
|
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);
|
|
48865
49960
|
}
|
|
48866
49961
|
const overrideExclude = overrideList(EXCLUDE_KEY);
|
|
48867
49962
|
const { allowed, exclude } = buildToolLists({
|
|
@@ -48869,6 +49964,16 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48869
49964
|
overrideShellAllowPatterns,
|
|
48870
49965
|
logger
|
|
48871
49966
|
});
|
|
49967
|
+
warnAboutGlobSpelledOverrideEntries({
|
|
49968
|
+
entries: globSpelledOverrideEntries,
|
|
49969
|
+
allowed,
|
|
49970
|
+
logger
|
|
49971
|
+
});
|
|
49972
|
+
const writableOverrideAllowed = withholdDeniedOverrideAllowed({
|
|
49973
|
+
overrideVerbatimAllowed,
|
|
49974
|
+
exclude,
|
|
49975
|
+
logger
|
|
49976
|
+
});
|
|
48872
49977
|
const ignoredOverrideKeys = Object.keys(override).filter((key) => key !== TOOLS_KEY && key !== GENERAL_KEY);
|
|
48873
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.`);
|
|
48874
49979
|
const existingTools = this.existingToolsGroup(existingContent);
|
|
@@ -48879,7 +49984,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48879
49984
|
});
|
|
48880
49985
|
const allowedList = uniq([
|
|
48881
49986
|
...allowed,
|
|
48882
|
-
...
|
|
49987
|
+
...writableOverrideAllowed,
|
|
48883
49988
|
...preservedEntries(ALLOWED_KEY)
|
|
48884
49989
|
]);
|
|
48885
49990
|
const excludeList = uniq([
|
|
@@ -48887,12 +49992,12 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48887
49992
|
...overrideExclude,
|
|
48888
49993
|
...preservedEntries(EXCLUDE_KEY)
|
|
48889
49994
|
]);
|
|
48890
|
-
|
|
48891
|
-
|
|
48892
|
-
|
|
48893
|
-
|
|
49995
|
+
reportDroppedManagedEntries({
|
|
49996
|
+
existingTools,
|
|
49997
|
+
allowedList,
|
|
49998
|
+
excludeList,
|
|
49999
|
+
logger
|
|
48894
50000
|
});
|
|
48895
|
-
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.`);
|
|
48896
50001
|
const tools = {
|
|
48897
50002
|
...overrideTools,
|
|
48898
50003
|
[ALLOWED_KEY]: allowedList.length > 0 ? allowedList : void 0,
|
|
@@ -48906,7 +50011,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
|
|
|
48906
50011
|
entries: collectTrustAffectingOverrideEntries({
|
|
48907
50012
|
tools: {
|
|
48908
50013
|
...overrideTools,
|
|
48909
|
-
[ALLOWED_KEY]:
|
|
50014
|
+
[ALLOWED_KEY]: writableOverrideAllowed
|
|
48910
50015
|
},
|
|
48911
50016
|
general: overrideGeneral
|
|
48912
50017
|
}),
|
|
@@ -52263,6 +53368,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
52263
53368
|
supportsImport: true
|
|
52264
53369
|
}
|
|
52265
53370
|
}],
|
|
53371
|
+
["crush", {
|
|
53372
|
+
class: CrushPermissions,
|
|
53373
|
+
meta: {
|
|
53374
|
+
supportsProject: true,
|
|
53375
|
+
supportsGlobal: true,
|
|
53376
|
+
supportsImport: true
|
|
53377
|
+
}
|
|
53378
|
+
}],
|
|
52266
53379
|
["cursor", {
|
|
52267
53380
|
class: CursorPermissions,
|
|
52268
53381
|
meta: {
|
|
@@ -58284,6 +59397,183 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
58284
59397
|
}
|
|
58285
59398
|
};
|
|
58286
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
|
|
58287
59577
|
//#region src/features/skills/qwencode-skill.ts
|
|
58288
59578
|
const QwencodeSkillFrontmatterSchema = z.looseObject({
|
|
58289
59579
|
name: z.string(),
|
|
@@ -60170,6 +61460,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
60170
61460
|
supportsGlobal: true
|
|
60171
61461
|
}
|
|
60172
61462
|
}],
|
|
61463
|
+
["pool", {
|
|
61464
|
+
class: PoolSkill,
|
|
61465
|
+
meta: {
|
|
61466
|
+
supportsProject: true,
|
|
61467
|
+
supportsSimulated: false,
|
|
61468
|
+
supportsGlobal: true
|
|
61469
|
+
}
|
|
61470
|
+
}],
|
|
60173
61471
|
["qwencode", {
|
|
60174
61472
|
class: QwencodeSkill,
|
|
60175
61473
|
meta: {
|
|
@@ -65004,7 +66302,7 @@ var RooSubagent = class extends ToolSubagent {
|
|
|
65004
66302
|
//#region src/features/subagents/tabnine-subagent.ts
|
|
65005
66303
|
const TabnineSubagentFrontmatterSchema = z.looseObject({
|
|
65006
66304
|
name: z.string(),
|
|
65007
|
-
description: z.
|
|
66305
|
+
description: z.string(),
|
|
65008
66306
|
/** "local" (default) runs inside the CLI; "remote" delegates to a Tabnine cloud agent. */
|
|
65009
66307
|
kind: z.optional(z.string()),
|
|
65010
66308
|
/** Built-in tool names the subagent may use; omitted = every tool. */
|
|
@@ -65063,14 +66361,23 @@ var TabnineSubagent = class TabnineSubagent extends ToolSubagent {
|
|
|
65063
66361
|
validate: true
|
|
65064
66362
|
});
|
|
65065
66363
|
}
|
|
65066
|
-
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
66364
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
65067
66365
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
65068
66366
|
const tabnineSection = rulesyncFrontmatter.tabnine ?? {};
|
|
65069
|
-
const
|
|
66367
|
+
const merged = {
|
|
65070
66368
|
name: rulesyncFrontmatter.name,
|
|
65071
66369
|
description: rulesyncFrontmatter.description,
|
|
65072
66370
|
...tabnineSection
|
|
65073
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
|
+
};
|
|
65074
66381
|
const body = rulesyncSubagent.getBody();
|
|
65075
66382
|
const fileContent = stringifyFrontmatter(body, tabnineSubagentFrontmatter, { avoidBlockScalars: true });
|
|
65076
66383
|
const paths = this.getSettablePaths({ global });
|
|
@@ -71333,24 +72640,6 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
71333
72640
|
}
|
|
71334
72641
|
};
|
|
71335
72642
|
//#endregion
|
|
71336
|
-
//#region src/constants/pool-paths.ts
|
|
71337
|
-
/**
|
|
71338
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
71339
|
-
*
|
|
71340
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
71341
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
71342
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
71343
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
71344
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
71345
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
71346
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
71347
|
-
*
|
|
71348
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
71349
|
-
* @see https://github.com/poolsideai/pool
|
|
71350
|
-
*/
|
|
71351
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
71352
|
-
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
71353
|
-
//#endregion
|
|
71354
72643
|
//#region src/features/rules/pool-rule.ts
|
|
71355
72644
|
var PoolRule = class PoolRule extends ToolRule {
|
|
71356
72645
|
static getSettablePaths({ global = false } = {}) {
|
|
@@ -76417,6 +77706,6 @@ async function importChecksCore(params) {
|
|
|
76417
77706
|
return writtenCount;
|
|
76418
77707
|
}
|
|
76419
77708
|
//#endregion
|
|
76420
|
-
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 };
|
|
76421
77710
|
|
|
76422
|
-
//# sourceMappingURL=import-
|
|
77711
|
+
//# sourceMappingURL=import-C6R-lXFF.js.map
|