rulesync 16.6.0 → 16.8.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/dist/cli/index.cjs +43 -25
- package/dist/cli/index.js +43 -25
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-D87qlRUj.cjs → import-DEK1TMmW.cjs} +1408 -420
- package/dist/{import-BlwypG9v.js → import-KXnvmbzr.js} +1399 -417
- package/dist/import-KXnvmbzr.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +32 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BlwypG9v.js.map +0 -1
|
@@ -639,6 +639,13 @@ async function readFileBuffer(filepath) {
|
|
|
639
639
|
return (0, node_fs_promises.readFile)(filepath);
|
|
640
640
|
}
|
|
641
641
|
/**
|
|
642
|
+
* Read file as a buffer if it exists, otherwise return null.
|
|
643
|
+
*/
|
|
644
|
+
async function readFileBufferOrNull(filepath) {
|
|
645
|
+
if (await fileExists(filepath)) return readFileBuffer(filepath);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
642
649
|
* Normalizes text to LF line endings and adds exactly one trailing newline.
|
|
643
650
|
* Removes any existing trailing whitespace and appends a single newline.
|
|
644
651
|
*/
|
|
@@ -650,6 +657,10 @@ async function writeFileContent(filepath, content) {
|
|
|
650
657
|
await ensureDir((0, node_path.dirname)(filepath));
|
|
651
658
|
await (0, node_fs_promises.writeFile)(filepath, content, "utf-8");
|
|
652
659
|
}
|
|
660
|
+
async function writeFileBuffer(filepath, buffer) {
|
|
661
|
+
await ensureDir((0, node_path.dirname)(filepath));
|
|
662
|
+
await (0, node_fs_promises.writeFile)(filepath, buffer);
|
|
663
|
+
}
|
|
653
664
|
async function fileExists(filepath) {
|
|
654
665
|
try {
|
|
655
666
|
await (0, node_fs_promises.stat)(filepath);
|
|
@@ -1856,7 +1867,7 @@ var RulesyncFile = class extends AiFile {
|
|
|
1856
1867
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
1857
1868
|
* This excludes arrays and null values.
|
|
1858
1869
|
*/
|
|
1859
|
-
function isRecord(value) {
|
|
1870
|
+
function isRecord$1(value) {
|
|
1860
1871
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1861
1872
|
}
|
|
1862
1873
|
/**
|
|
@@ -1871,7 +1882,7 @@ function isRecord(value) {
|
|
|
1871
1882
|
* malicious accessor descriptors.
|
|
1872
1883
|
*/
|
|
1873
1884
|
function isPlainObject$1(value) {
|
|
1874
|
-
if (!isRecord(value)) return false;
|
|
1885
|
+
if (!isRecord$1(value)) return false;
|
|
1875
1886
|
const proto = Object.getPrototypeOf(value);
|
|
1876
1887
|
return proto === null || proto === Object.prototype;
|
|
1877
1888
|
}
|
|
@@ -2177,11 +2188,13 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
|
|
|
2177
2188
|
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
2178
2189
|
cacheTtl: zod_mini.z.optional(zod_mini.z.number().check((0, zod_mini.nonnegative)())),
|
|
2179
2190
|
matcher: zod_mini.z.optional(safeString),
|
|
2191
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2180
2192
|
prompt: zod_mini.z.optional(safeString),
|
|
2181
2193
|
loop_limit: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.number())),
|
|
2182
2194
|
name: zod_mini.z.optional(safeString),
|
|
2183
2195
|
description: zod_mini.z.optional(safeString),
|
|
2184
2196
|
failClosed: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2197
|
+
commandRegex: zod_mini.z.optional(safeString),
|
|
2185
2198
|
sequential: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2186
2199
|
async: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2187
2200
|
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), safeString)),
|
|
@@ -2198,6 +2211,7 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
|
|
|
2198
2211
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
2199
2212
|
if: zod_mini.z.optional(safeString),
|
|
2200
2213
|
commandWindows: zod_mini.z.optional(safeString),
|
|
2214
|
+
additionalContextLimit: zod_mini.z.optional(zod_mini.z.int().check((0, zod_mini.nonnegative)())),
|
|
2201
2215
|
asyncRewake: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2202
2216
|
continueOnBlock: zod_mini.z.optional(zod_mini.z.boolean())
|
|
2203
2217
|
});
|
|
@@ -2493,8 +2507,9 @@ const FACTORYDROID_HOOK_EVENTS = [
|
|
|
2493
2507
|
/**
|
|
2494
2508
|
* Hook events supported by deepagents-cli (`deepagents-code` / `dcode`).
|
|
2495
2509
|
*
|
|
2496
|
-
*
|
|
2497
|
-
*
|
|
2510
|
+
* These are the twelve Hooks v2 `HookEvent` members, GA since deepagents-code
|
|
2511
|
+
* 0.1.52. Canonical `contextOffload` is deliberately absent — see
|
|
2512
|
+
* {@link CANONICAL_TO_DEEPAGENTS_EVENT_NAMES}.
|
|
2498
2513
|
* https://docs.langchain.com/oss/python/deepagents/cli/configuration
|
|
2499
2514
|
*/
|
|
2500
2515
|
const DEEPAGENTS_HOOK_EVENTS = [
|
|
@@ -2507,8 +2522,9 @@ const DEEPAGENTS_HOOK_EVENTS = [
|
|
|
2507
2522
|
"postToolUseFailure",
|
|
2508
2523
|
"stop",
|
|
2509
2524
|
"preCompact",
|
|
2510
|
-
"
|
|
2511
|
-
"
|
|
2525
|
+
"notification",
|
|
2526
|
+
"subagentStart",
|
|
2527
|
+
"subagentStop"
|
|
2512
2528
|
];
|
|
2513
2529
|
/** Hook events supported by Codex CLI. */
|
|
2514
2530
|
const CODEXCLI_HOOK_EVENTS = [
|
|
@@ -3196,26 +3212,59 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
|
|
|
3196
3212
|
*/
|
|
3197
3213
|
const GOOSE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GOOSE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
3198
3214
|
/**
|
|
3199
|
-
* Map canonical camelCase event names to deepagents-cli
|
|
3215
|
+
* Map canonical camelCase event names to the deepagents-cli Hooks v2
|
|
3216
|
+
* `HookEvent` values.
|
|
3217
|
+
*
|
|
3218
|
+
* Hooks v2 went GA in deepagents-code 0.1.52 (2026-08-04) and replaced the
|
|
3219
|
+
* legacy dot-notation names (`session.start`, `tool.use`, …) with these twelve
|
|
3220
|
+
* PascalCase members. The legacy list format is still read, but is scheduled for
|
|
3221
|
+
* removal on 2026-09-01 (`_LEGACY_HOOKS_REMOVAL_DATE` in `hooks/loading.py`).
|
|
3222
|
+
*
|
|
3223
|
+
* Canonical `contextOffload` has no v2 counterpart. Its legacy event
|
|
3224
|
+
* (`context.offload`) is gone, and folding it onto `PreCompact` would silently
|
|
3225
|
+
* merge two distinct canonical events into one — so it is dropped for
|
|
3226
|
+
* deepagents instead, and reported by the hooks processor as an unsupported
|
|
3227
|
+
* event like any other.
|
|
3228
|
+
*
|
|
3229
|
+
* @see https://github.com/langchain-ai/deepagents `libs/code/deepagents_code/hooks/models/domain.py`
|
|
3200
3230
|
*/
|
|
3201
3231
|
const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
|
|
3202
|
-
sessionStart: "
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
permissionRequest: "
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
preCompact: "
|
|
3211
|
-
|
|
3212
|
-
|
|
3232
|
+
sessionStart: "SessionStart",
|
|
3233
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
3234
|
+
sessionEnd: "SessionEnd",
|
|
3235
|
+
permissionRequest: "PermissionRequest",
|
|
3236
|
+
notification: "Notification",
|
|
3237
|
+
preToolUse: "PreToolUse",
|
|
3238
|
+
postToolUse: "PostToolUse",
|
|
3239
|
+
postToolUseFailure: "PostToolUseFailure",
|
|
3240
|
+
preCompact: "PreCompact",
|
|
3241
|
+
stop: "Stop",
|
|
3242
|
+
subagentStart: "SubagentStart",
|
|
3243
|
+
subagentStop: "SubagentStop"
|
|
3213
3244
|
};
|
|
3214
3245
|
/**
|
|
3215
|
-
* Map deepagents-cli
|
|
3246
|
+
* Map deepagents-cli `HookEvent` values to canonical camelCase.
|
|
3216
3247
|
*/
|
|
3217
3248
|
const DEEPAGENTS_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEEPAGENTS_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
3218
3249
|
/**
|
|
3250
|
+
* The legacy dot-notation event names deepagents-cli used before Hooks v2.
|
|
3251
|
+
* Kept for the read-only import path so a `hooks.json` still in the old format
|
|
3252
|
+
* round-trips into canonical events instead of being silently discarded.
|
|
3253
|
+
*/
|
|
3254
|
+
const DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES = {
|
|
3255
|
+
"session.start": "sessionStart",
|
|
3256
|
+
"session.end": "sessionEnd",
|
|
3257
|
+
"user.prompt": "beforeSubmitPrompt",
|
|
3258
|
+
"permission.request": "permissionRequest",
|
|
3259
|
+
"tool.use": "preToolUse",
|
|
3260
|
+
"tool.result": "postToolUse",
|
|
3261
|
+
"tool.error": "postToolUseFailure",
|
|
3262
|
+
"task.complete": "stop",
|
|
3263
|
+
"context.compact": "preCompact",
|
|
3264
|
+
"context.offload": "contextOffload",
|
|
3265
|
+
"input.required": "notification"
|
|
3266
|
+
};
|
|
3267
|
+
/**
|
|
3219
3268
|
* Map canonical camelCase event names to Kiro CLI camelCase.
|
|
3220
3269
|
* Kiro CLI uses its own event naming: agentSpawn, userPromptSubmit, preToolUse,
|
|
3221
3270
|
* postToolUse, stop. Both `sessionEnd` and `stop` canonical events map to
|
|
@@ -3833,7 +3882,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
3833
3882
|
logger
|
|
3834
3883
|
});
|
|
3835
3884
|
for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
|
|
3836
|
-
if (!isRecord(json[ignoredKey])) continue;
|
|
3885
|
+
if (!isRecord$1(json[ignoredKey])) continue;
|
|
3837
3886
|
this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
|
|
3838
3887
|
}
|
|
3839
3888
|
const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
|
|
@@ -3845,7 +3894,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
3845
3894
|
}));
|
|
3846
3895
|
for (const blockKey of blockKeys) {
|
|
3847
3896
|
const toolBlock = json[blockKey];
|
|
3848
|
-
const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
|
|
3897
|
+
const toolServers = isRecord$1(toolBlock) && isRecord$1(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
|
|
3849
3898
|
for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
|
|
3850
3899
|
if (isPrototypePollutionKey(serverName)) continue;
|
|
3851
3900
|
if (serverConfig === null) delete effectiveServers[serverName];
|
|
@@ -4245,7 +4294,8 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4245
4294
|
*/
|
|
4246
4295
|
const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
4247
4296
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
4248
|
-
commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
4297
|
+
commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
4298
|
+
disabledSkills: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
4249
4299
|
});
|
|
4250
4300
|
/**
|
|
4251
4301
|
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
@@ -4691,13 +4741,22 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
|
|
|
4691
4741
|
* `base_permission_profile` it is consumed by the profile builder, not
|
|
4692
4742
|
* written as a top-level config key.
|
|
4693
4743
|
*
|
|
4694
|
-
*
|
|
4695
|
-
*
|
|
4696
|
-
*
|
|
4697
|
-
*
|
|
4698
|
-
*
|
|
4699
|
-
*
|
|
4700
|
-
*
|
|
4744
|
+
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
4745
|
+
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
4746
|
+
* (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
|
|
4747
|
+
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
|
|
4748
|
+
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
4749
|
+
* `base_permission_profile` and `git_write_rules` are consumed by the profile
|
|
4750
|
+
* builder rather than written, as described above, and `permission` is the
|
|
4751
|
+
* tool-scoped canonical block, which `RulesyncPermissions.forTarget` strips out
|
|
4752
|
+
* of the override before it ever reaches the patch. The allowlist is what keeps
|
|
4753
|
+
* the override from clobbering a feature-owned key: `mcp_servers.*` per-MCP
|
|
4754
|
+
* gating is owned by the MCP feature (`codexcli-mcp.ts` already writes the
|
|
4755
|
+
* `mcp_servers` tables in the same `config.toml`), and `permissions` /
|
|
4756
|
+
* `default_permissions` are owned by the canonical model. The schema itself is
|
|
4757
|
+
* `looseObject` so an unmodeled key parses (and is then reported rather than
|
|
4758
|
+
* rejected outright); supporting a new top-level Codex config key means adding
|
|
4759
|
+
* it to `CODEXCLI_OVERRIDE_KEYS`.
|
|
4701
4760
|
*
|
|
4702
4761
|
* @see https://developers.openai.com/codex/config-reference
|
|
4703
4762
|
* @see https://developers.openai.com/codex/permissions
|
|
@@ -4852,9 +4911,9 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
4852
4911
|
if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
|
|
4853
4912
|
const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
|
|
4854
4913
|
const json = this.json;
|
|
4855
|
-
if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
|
|
4914
|
+
if (overrideKey !== toolTarget && isRecord$1(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
|
|
4856
4915
|
const overrideBlock = json[overrideKey];
|
|
4857
|
-
if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
|
|
4916
|
+
if (!isRecord$1(overrideBlock) || !isRecord$1(overrideBlock.permission)) return this;
|
|
4858
4917
|
const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
|
|
4859
4918
|
const merged = {
|
|
4860
4919
|
...json,
|
|
@@ -5164,6 +5223,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5164
5223
|
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
5165
5224
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
5166
5225
|
})),
|
|
5226
|
+
kiro: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5227
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
5228
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
5229
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
5230
|
+
})),
|
|
5167
5231
|
deepagents: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5168
5232
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
5169
5233
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -5172,7 +5236,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5172
5236
|
})),
|
|
5173
5237
|
copilot: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5174
5238
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
5175
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
5239
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
5240
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string()),
|
|
5241
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5242
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5243
|
+
context: zod_mini.z.optional(zod_mini.z.string())
|
|
5176
5244
|
})),
|
|
5177
5245
|
copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5178
5246
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -5230,7 +5298,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5230
5298
|
})),
|
|
5231
5299
|
factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5232
5300
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5233
|
-
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean())
|
|
5301
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5302
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5303
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
5234
5304
|
})),
|
|
5235
5305
|
grokcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5236
5306
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
@@ -5443,8 +5513,8 @@ async function getLocalSkillDirNames(outputRoot) {
|
|
|
5443
5513
|
* Resolve the effective `disable-model-invocation` value for a tool skill.
|
|
5444
5514
|
*
|
|
5445
5515
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
5446
|
-
* default that applies to every tool supporting the flag (claudecode,
|
|
5447
|
-
* zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5516
|
+
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
5517
|
+
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5448
5518
|
* default with a per-target value. A defined section value (including `false`)
|
|
5449
5519
|
* always wins over the root default.
|
|
5450
5520
|
*
|
|
@@ -5457,8 +5527,8 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
5457
5527
|
* Resolve the effective `user-invocable` value for a tool skill.
|
|
5458
5528
|
*
|
|
5459
5529
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
5460
|
-
* that applies to every tool supporting the flag (claudecode,
|
|
5461
|
-
* grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5530
|
+
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
5531
|
+
* copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5462
5532
|
* per-target value. A defined section value (including `false`) always wins
|
|
5463
5533
|
* over the root default.
|
|
5464
5534
|
*
|
|
@@ -5671,6 +5741,31 @@ function fileContentsEquivalent({ filePath, expected, existing }) {
|
|
|
5671
5741
|
if (structured !== void 0) return structured;
|
|
5672
5742
|
return addTrailingNewline(expected) === addTrailingNewline(existing);
|
|
5673
5743
|
}
|
|
5744
|
+
/**
|
|
5745
|
+
* Whether an on-disk companion file is equivalent to the generated one.
|
|
5746
|
+
*
|
|
5747
|
+
* Companion files (everything beside a skill's `SKILL.md`) are written byte for
|
|
5748
|
+
* byte, so byte equality is the whole test for a user asset carried through
|
|
5749
|
+
* from the source directory: a CRLF fixture or a deliberately newline-less file
|
|
5750
|
+
* must compare equal to itself and unequal to a normalized copy, and a copy
|
|
5751
|
+
* that has drifted must be repaired rather than tolerated.
|
|
5752
|
+
*
|
|
5753
|
+
* A `composed` file is different — Rulesync builds it from frontmatter (Codex
|
|
5754
|
+
* CLI's `agents/openai.yaml`), so differing bytes fall back to the structured
|
|
5755
|
+
* comparison and a formatter re-indenting it is not reported as a change on
|
|
5756
|
+
* every generate. Only the structured verdict counts: there is deliberately no
|
|
5757
|
+
* text fallback, since trailing-whitespace-insensitive text equality is exactly
|
|
5758
|
+
* the normalization companion files no longer get.
|
|
5759
|
+
*/
|
|
5760
|
+
function companionFileContentsEquivalent({ filePath, expected, existing, composed = false }) {
|
|
5761
|
+
if (existing === null) return false;
|
|
5762
|
+
if (existing.equals(expected)) return true;
|
|
5763
|
+
if (!composed) return false;
|
|
5764
|
+
const expectedText = expected.toString("utf-8");
|
|
5765
|
+
const existingText = existing.toString("utf-8");
|
|
5766
|
+
if (!Buffer.from(expectedText, "utf-8").equals(expected) || !Buffer.from(existingText, "utf-8").equals(existing)) return false;
|
|
5767
|
+
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
5768
|
+
}
|
|
5674
5769
|
//#endregion
|
|
5675
5770
|
//#region src/types/feature-processor.ts
|
|
5676
5771
|
var FeatureProcessor = class {
|
|
@@ -7071,6 +7166,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7071
7166
|
ownedKeys: ["mcp", "tools"]
|
|
7072
7167
|
} }
|
|
7073
7168
|
},
|
|
7169
|
+
".config/goose/config.yaml": {
|
|
7170
|
+
format: "yaml",
|
|
7171
|
+
invalidRootPolicy: "error",
|
|
7172
|
+
features: { mcp: {
|
|
7173
|
+
kind: "replace-owned-keys",
|
|
7174
|
+
ownedKeys: ["extensions"]
|
|
7175
|
+
} }
|
|
7176
|
+
},
|
|
7074
7177
|
[CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
|
|
7075
7178
|
format: "toml",
|
|
7076
7179
|
features: {
|
|
@@ -8219,7 +8322,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
8219
8322
|
}
|
|
8220
8323
|
static extractAntigravityConfig(rulesyncCommand) {
|
|
8221
8324
|
const antigravity = rulesyncCommand.getFrontmatter().antigravity;
|
|
8222
|
-
return isRecord(antigravity) ? antigravity : void 0;
|
|
8325
|
+
return isRecord$1(antigravity) ? antigravity : void 0;
|
|
8223
8326
|
}
|
|
8224
8327
|
static resolveTrigger(rulesyncCommand, antigravityConfig) {
|
|
8225
8328
|
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
@@ -9309,9 +9412,10 @@ const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
|
9309
9412
|
//#region src/features/commands/factorydroid-command.ts
|
|
9310
9413
|
const FactorydroidCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
9311
9414
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
9312
|
-
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
9313
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
9415
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
9314
9416
|
});
|
|
9417
|
+
/** Not a Droid command surface; see the schema comment above. */
|
|
9418
|
+
const FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS = ["allowed-tools"];
|
|
9315
9419
|
var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
9316
9420
|
frontmatter;
|
|
9317
9421
|
body;
|
|
@@ -9338,6 +9442,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9338
9442
|
}
|
|
9339
9443
|
toRulesyncCommand() {
|
|
9340
9444
|
const { description, ...restFields } = this.frontmatter;
|
|
9445
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete restFields[field];
|
|
9341
9446
|
const rulesyncFrontmatter = {
|
|
9342
9447
|
targets: ["*"],
|
|
9343
9448
|
description,
|
|
@@ -9361,6 +9466,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9361
9466
|
description: rulesyncFrontmatter.description,
|
|
9362
9467
|
...factorydroidFields
|
|
9363
9468
|
};
|
|
9469
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete factorydroidFrontmatter[field];
|
|
9364
9470
|
const body = rulesyncCommand.getBody();
|
|
9365
9471
|
const paths = this.getSettablePaths({ global });
|
|
9366
9472
|
return new FactorydroidCommand({
|
|
@@ -12006,8 +12112,8 @@ async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
|
|
|
12006
12112
|
}
|
|
12007
12113
|
if (!isPlainObject$1(parsed) || !Array.isArray(parsed.prompts)) return "";
|
|
12008
12114
|
const expectedContentFile = toPosixPath((0, node_path.join)("prompts", relativeFilePath));
|
|
12009
|
-
const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
|
|
12010
|
-
return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
|
|
12115
|
+
const entry = parsed.prompts.find((candidate) => isRecord$1(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
|
|
12116
|
+
return isRecord$1(entry) && typeof entry.description === "string" ? entry.description : "";
|
|
12011
12117
|
}
|
|
12012
12118
|
/**
|
|
12013
12119
|
* The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
|
|
@@ -13072,16 +13178,28 @@ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
|
|
|
13072
13178
|
};
|
|
13073
13179
|
}
|
|
13074
13180
|
/**
|
|
13075
|
-
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13076
|
-
* preserving insertion order of both keys and grouped definitions.
|
|
13181
|
+
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13182
|
+
* absent), preserving insertion order of both keys and grouped definitions.
|
|
13183
|
+
* Definitions that disagree on a `subdividesGroup` passthrough field are split
|
|
13184
|
+
* into separate groups, so a restricting field is never inherited by a hook
|
|
13185
|
+
* that did not ask for it.
|
|
13077
13186
|
*/
|
|
13078
|
-
function groupDefinitionsByMatcher(definitions) {
|
|
13187
|
+
function groupDefinitionsByMatcher({ definitions, converterConfig }) {
|
|
13188
|
+
const subdividingFields = (converterConfig.groupPassthroughFields ?? []).filter(({ subdividesGroup }) => subdividesGroup);
|
|
13079
13189
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
13080
13190
|
for (const def of definitions) {
|
|
13081
|
-
const
|
|
13082
|
-
const
|
|
13083
|
-
|
|
13084
|
-
|
|
13191
|
+
const rawMatcher = def.matcher ?? "";
|
|
13192
|
+
const matcher = converterConfig.wildcardMatcherMeansAll && rawMatcher === "*" ? "" : rawMatcher;
|
|
13193
|
+
const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
|
|
13194
|
+
const value = def[canonical];
|
|
13195
|
+
return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
13196
|
+
})].join("\0");
|
|
13197
|
+
const group = byMatcher.get(key);
|
|
13198
|
+
if (group) group.defs.push(def);
|
|
13199
|
+
else byMatcher.set(key, {
|
|
13200
|
+
matcher,
|
|
13201
|
+
defs: [def]
|
|
13202
|
+
});
|
|
13085
13203
|
}
|
|
13086
13204
|
return byMatcher;
|
|
13087
13205
|
}
|
|
@@ -13108,59 +13226,148 @@ function applyCommandPrefix({ def, converterConfig }) {
|
|
|
13108
13226
|
return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
|
|
13109
13227
|
}
|
|
13110
13228
|
/**
|
|
13111
|
-
*
|
|
13112
|
-
*
|
|
13113
|
-
*
|
|
13229
|
+
* Whether a field registered for `command` hooks only applies to this hook.
|
|
13230
|
+
* Applied on both export and import: a value imported into a canonical field
|
|
13231
|
+
* the exporter would then drop is silently deleted on the next generate.
|
|
13114
13232
|
*/
|
|
13115
|
-
function
|
|
13116
|
-
return
|
|
13117
|
-
if (commandOnly === true && hookType !== "command") return false;
|
|
13118
|
-
return typeof def[canonical] === "boolean";
|
|
13119
|
-
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13233
|
+
function isFieldApplicable({ commandOnly, hookType }) {
|
|
13234
|
+
return commandOnly !== true || hookType === "command";
|
|
13120
13235
|
}
|
|
13121
13236
|
/**
|
|
13122
|
-
*
|
|
13123
|
-
*
|
|
13124
|
-
|
|
13125
|
-
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
*
|
|
13130
|
-
*
|
|
13131
|
-
*
|
|
13132
|
-
*/
|
|
13133
|
-
function emitStringPassthroughFields({ def, hookType, converterConfig }) {
|
|
13134
|
-
return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13135
|
-
if (commandOnly === true && hookType !== "command") return false;
|
|
13136
|
-
return typeof def[canonical] === "string" && def[canonical] !== "";
|
|
13137
|
-
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13138
|
-
}
|
|
13139
|
-
/**
|
|
13140
|
-
* Import the configured string passthrough fields back into canonical fields,
|
|
13141
|
-
* reversing {@link emitStringPassthroughFields}. Only non-empty string values
|
|
13142
|
-
* are read.
|
|
13143
|
-
*/
|
|
13144
|
-
function importStringPassthroughFields({ h, converterConfig }) {
|
|
13145
|
-
return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "string" && h[tool] !== "").map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13146
|
-
}
|
|
13147
|
-
/**
|
|
13148
|
-
* Emit the configured string-array passthrough fields on the tool side.
|
|
13149
|
-
*/
|
|
13150
|
-
function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
|
|
13151
|
-
return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13152
|
-
if (commandOnly === true && hookType !== "command") return false;
|
|
13153
|
-
return isStringArray(def[canonical]);
|
|
13154
|
-
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13155
|
-
}
|
|
13156
|
-
/**
|
|
13157
|
-
* Import the configured string-array passthrough fields, reversing
|
|
13158
|
-
* {@link emitArrayPassthroughFields}.
|
|
13237
|
+
* Emit the configured passthrough fields on the tool side, mapping each
|
|
13238
|
+
* canonical field name to its (possibly renamed) tool field name. Only values
|
|
13239
|
+
* accepted by `isValid` are carried through, so a malformed field can't leak
|
|
13240
|
+
* into a config the tool would reject.
|
|
13241
|
+
*
|
|
13242
|
+
* A field the tool documents on `command` hooks only is dropped when authored
|
|
13243
|
+
* on another hook type. That is a value the user hand-wrote in
|
|
13244
|
+
* `.rulesync/hooks.*` and the canonical schema does not cross-validate it
|
|
13245
|
+
* against the hook's type, so it is warned about rather than deleted in
|
|
13246
|
+
* silence — the mirror of the import side.
|
|
13159
13247
|
*/
|
|
13160
|
-
function
|
|
13161
|
-
const
|
|
13162
|
-
|
|
13163
|
-
|
|
13248
|
+
function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn }) {
|
|
13249
|
+
for (const { canonical, tool, commandOnly } of fields) {
|
|
13250
|
+
const value = def[canonical];
|
|
13251
|
+
if (value === void 0) continue;
|
|
13252
|
+
if (!isFieldApplicable({
|
|
13253
|
+
commandOnly,
|
|
13254
|
+
hookType
|
|
13255
|
+
})) {
|
|
13256
|
+
warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not generated.`);
|
|
13257
|
+
continue;
|
|
13258
|
+
}
|
|
13259
|
+
if (!isValid({
|
|
13260
|
+
value,
|
|
13261
|
+
canonical
|
|
13262
|
+
})) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${JSON.stringify(value)} is not a value this tool can express as "${tool}".`);
|
|
13263
|
+
}
|
|
13264
|
+
return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
|
|
13265
|
+
commandOnly,
|
|
13266
|
+
hookType
|
|
13267
|
+
}) && isValid({
|
|
13268
|
+
value: def[canonical],
|
|
13269
|
+
canonical
|
|
13270
|
+
})).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13271
|
+
}
|
|
13272
|
+
/**
|
|
13273
|
+
* Import the configured passthrough fields back into canonical fields,
|
|
13274
|
+
* reversing {@link emitPassthroughFields}. A field the tool documents on
|
|
13275
|
+
* `command` hooks only is skipped here too, and `describeInvalid` — when the
|
|
13276
|
+
* kind has a rule an authored file can plausibly violate — turns a rejected
|
|
13277
|
+
* value into a warning instead of a silent drop.
|
|
13278
|
+
*/
|
|
13279
|
+
function importPassthroughFields({ h, hookType, fields, isValid, describeInvalid, warn }) {
|
|
13280
|
+
const applicable = fields.filter(({ commandOnly }) => isFieldApplicable({
|
|
13281
|
+
commandOnly,
|
|
13282
|
+
hookType
|
|
13283
|
+
}));
|
|
13284
|
+
const skipped = fields.filter(({ commandOnly }) => !isFieldApplicable({
|
|
13285
|
+
commandOnly,
|
|
13286
|
+
hookType
|
|
13287
|
+
}));
|
|
13288
|
+
for (const { tool } of skipped) if (h[tool] !== void 0) warn?.(`Dropping "${tool}" from an imported "${hookType}" hook: this tool documents it on "command" hooks only, so it is not imported.`);
|
|
13289
|
+
for (const { tool, canonical } of applicable) if (describeInvalid !== void 0 && h[tool] !== void 0 && !isValid({
|
|
13290
|
+
value: h[tool],
|
|
13291
|
+
canonical
|
|
13292
|
+
})) warn?.(describeInvalid({
|
|
13293
|
+
tool,
|
|
13294
|
+
canonical,
|
|
13295
|
+
value: h[tool]
|
|
13296
|
+
}));
|
|
13297
|
+
return Object.fromEntries(applicable.filter(({ tool, canonical }) => isValid({
|
|
13298
|
+
value: h[tool],
|
|
13299
|
+
canonical
|
|
13300
|
+
})).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13301
|
+
}
|
|
13302
|
+
const isBooleanValue = ({ value }) => typeof value === "boolean";
|
|
13303
|
+
const isFiniteNumber = ({ value }) => Number.isFinite(value);
|
|
13304
|
+
const isNonEmptyString = (value) => typeof value === "string" && value !== "";
|
|
13305
|
+
const isEmittableString = ({ value }) => isNonEmptyString(value);
|
|
13306
|
+
const isEmittableArray = ({ value }) => isStringArray(value);
|
|
13307
|
+
const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
|
|
13308
|
+
const isImportableArray = ({ value }) => isSafeStringArray(value);
|
|
13309
|
+
/**
|
|
13310
|
+
* The canonical schema of one hook field, looked up by name. Read off the
|
|
13311
|
+
* schema's own shape rather than by parsing a one-field object, so a `canonical`
|
|
13312
|
+
* name that no longer exists resolves to `undefined` (a `looseObject` would
|
|
13313
|
+
* accept an unknown key and silently validate nothing) and a typo is caught by
|
|
13314
|
+
* the tests instead of quietly disabling the check.
|
|
13315
|
+
*/
|
|
13316
|
+
const CANONICAL_FIELD_SCHEMAS = HookDefinitionSchema.def.shape;
|
|
13317
|
+
/**
|
|
13318
|
+
* Whether a value satisfies the constraints the canonical schema puts on the
|
|
13319
|
+
* field it would be imported into.
|
|
13320
|
+
*
|
|
13321
|
+
* Import needs this on top of the kind's shape check: a hand-written tool
|
|
13322
|
+
* settings file can hold `"shell": "zsh"`, which is a non-empty string but not
|
|
13323
|
+
* a member of the canonical `z.enum(["bash", "powershell"])`. Letting it in
|
|
13324
|
+
* would write a `.rulesync/hooks.jsonc` that fails validation on the *next*
|
|
13325
|
+
* run, taking the whole hooks feature down with it.
|
|
13326
|
+
*/
|
|
13327
|
+
function satisfiesCanonicalField({ value, canonical }) {
|
|
13328
|
+
const schema = canonicalFieldSchema(canonical);
|
|
13329
|
+
return schema !== void 0 && zod_mini.z.safeParse(schema, value).success;
|
|
13330
|
+
}
|
|
13331
|
+
/** Own properties only, so a name like `toString` resolves to nothing. */
|
|
13332
|
+
function canonicalFieldSchema(canonical) {
|
|
13333
|
+
return Object.hasOwn(CANONICAL_FIELD_SCHEMAS, canonical) ? CANONICAL_FIELD_SCHEMAS[canonical] : void 0;
|
|
13334
|
+
}
|
|
13335
|
+
const isImportableString = ({ value, canonical }) => isNonEmptyString(value) && satisfiesCanonicalField({
|
|
13336
|
+
value,
|
|
13337
|
+
canonical
|
|
13338
|
+
});
|
|
13339
|
+
const isImportableNumber = ({ value, canonical }) => Number.isFinite(value) && satisfiesCanonicalField({
|
|
13340
|
+
value,
|
|
13341
|
+
canonical
|
|
13342
|
+
});
|
|
13343
|
+
/**
|
|
13344
|
+
* Say which rule the value broke, so the warning names the actual constraint
|
|
13345
|
+
* rather than asserting a canonical rejection that may not be the reason. A
|
|
13346
|
+
* closed enum lists its members; a rule carrying its own message (the
|
|
13347
|
+
* control-character check behind `safeString`) reuses it.
|
|
13348
|
+
*/
|
|
13349
|
+
function describeScalarConstraint({ canonical, value }) {
|
|
13350
|
+
const schema = canonicalFieldSchema(canonical);
|
|
13351
|
+
const result = schema === void 0 ? void 0 : zod_mini.z.safeParse(schema, value);
|
|
13352
|
+
if (result === void 0 || result.success) return `it is not a value this field carries through.`;
|
|
13353
|
+
const issue = result.error.issues[0];
|
|
13354
|
+
if (issue === void 0) return `it is not a value the canonical "${canonical}" field accepts.`;
|
|
13355
|
+
return `it does not satisfy the canonical "${canonical}" field: ${issue.message}.`;
|
|
13356
|
+
}
|
|
13357
|
+
const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${JSON.stringify(value)}) while importing a hook: ${describeScalarConstraint({
|
|
13358
|
+
canonical,
|
|
13359
|
+
value
|
|
13360
|
+
})} Importing it would fail validation on the next run.`;
|
|
13361
|
+
const describeInvalidArray = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a list of strings without newline, carriage return or NUL characters.`;
|
|
13362
|
+
const describeInvalidRecord = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a map of strings whose keys are non-empty and free of "=", and whose keys and values carry no newline, carriage return or NUL characters.`;
|
|
13363
|
+
/**
|
|
13364
|
+
* Check a value against the shape its field documents. A string field also
|
|
13365
|
+
* rejects control characters, matching the canonical `safeString` so an
|
|
13366
|
+
* imported value cannot fail validation on the next generate.
|
|
13367
|
+
*/
|
|
13368
|
+
function isGroupPassthroughValue(value, valueType = "object") {
|
|
13369
|
+
if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
|
|
13370
|
+
return isPlainObject$1(value);
|
|
13164
13371
|
}
|
|
13165
13372
|
/**
|
|
13166
13373
|
* Emit the configured group-level passthrough fields, taken from the first
|
|
@@ -13168,12 +13375,12 @@ function importArrayPassthroughFields({ h, converterConfig, logger }) {
|
|
|
13168
13375
|
*/
|
|
13169
13376
|
function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
|
|
13170
13377
|
const emitted = {};
|
|
13171
|
-
for (const { canonical, tool } of converterConfig.groupPassthroughFields ?? []) {
|
|
13378
|
+
for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
|
|
13172
13379
|
const carried = defs.map((def) => def[canonical]);
|
|
13173
|
-
const first = carried.find((value) =>
|
|
13380
|
+
const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
|
|
13174
13381
|
if (first === void 0) continue;
|
|
13175
13382
|
const firstStable = stableJson(first);
|
|
13176
|
-
const agrees = (value) =>
|
|
13383
|
+
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
13177
13384
|
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${JSON.stringify(first)} — including any that asked for something else, or for nothing.`);
|
|
13178
13385
|
emitted[tool] = first;
|
|
13179
13386
|
}
|
|
@@ -13185,7 +13392,7 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
13185
13392
|
*/
|
|
13186
13393
|
function importGroupPassthroughFields({ rawEntry, converterConfig }) {
|
|
13187
13394
|
const entry = rawEntry;
|
|
13188
|
-
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool }) =>
|
|
13395
|
+
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
13189
13396
|
}
|
|
13190
13397
|
/**
|
|
13191
13398
|
* Emit the payload fields specific to a hook type — `url`/`headers`/
|
|
@@ -13213,7 +13420,54 @@ function emitTypePayloadFields({ def, hookType, converterConfig }) {
|
|
|
13213
13420
|
function isSupportedHookType({ type, converterConfig }) {
|
|
13214
13421
|
return converterConfig.supportedHookTypes?.has(type ?? "command") ?? true;
|
|
13215
13422
|
}
|
|
13216
|
-
|
|
13423
|
+
/**
|
|
13424
|
+
* Emit every per-hook passthrough kind for one canonical definition.
|
|
13425
|
+
*/
|
|
13426
|
+
function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, warn }) {
|
|
13427
|
+
return {
|
|
13428
|
+
...emitPassthroughFields({
|
|
13429
|
+
def,
|
|
13430
|
+
hookType,
|
|
13431
|
+
eventName,
|
|
13432
|
+
fields: converterConfig.booleanPassthroughFields ?? [],
|
|
13433
|
+
isValid: isBooleanValue,
|
|
13434
|
+
warn
|
|
13435
|
+
}),
|
|
13436
|
+
...emitPassthroughFields({
|
|
13437
|
+
def,
|
|
13438
|
+
hookType,
|
|
13439
|
+
eventName,
|
|
13440
|
+
fields: converterConfig.numberPassthroughFields ?? [],
|
|
13441
|
+
isValid: isFiniteNumber,
|
|
13442
|
+
warn
|
|
13443
|
+
}),
|
|
13444
|
+
...emitPassthroughFields({
|
|
13445
|
+
def,
|
|
13446
|
+
hookType,
|
|
13447
|
+
eventName,
|
|
13448
|
+
fields: converterConfig.stringPassthroughFields ?? [],
|
|
13449
|
+
isValid: isEmittableString,
|
|
13450
|
+
warn
|
|
13451
|
+
}),
|
|
13452
|
+
...emitPassthroughFields({
|
|
13453
|
+
def,
|
|
13454
|
+
hookType,
|
|
13455
|
+
eventName,
|
|
13456
|
+
fields: converterConfig.arrayPassthroughFields ?? [],
|
|
13457
|
+
isValid: isEmittableArray,
|
|
13458
|
+
warn
|
|
13459
|
+
}),
|
|
13460
|
+
...emitPassthroughFields({
|
|
13461
|
+
def,
|
|
13462
|
+
hookType,
|
|
13463
|
+
eventName,
|
|
13464
|
+
fields: converterConfig.recordPassthroughFields ?? [],
|
|
13465
|
+
isValid: isEmittableRecord,
|
|
13466
|
+
warn
|
|
13467
|
+
})
|
|
13468
|
+
};
|
|
13469
|
+
}
|
|
13470
|
+
function buildToolHooks({ defs, eventName, converterConfig, warn }) {
|
|
13217
13471
|
const hooks = [];
|
|
13218
13472
|
for (const def of defs) {
|
|
13219
13473
|
const hookType = def.type ?? "command";
|
|
@@ -13226,20 +13480,12 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13226
13480
|
converterConfig
|
|
13227
13481
|
});
|
|
13228
13482
|
hooks.push({
|
|
13229
|
-
...
|
|
13483
|
+
...emitAllPassthroughFields({
|
|
13230
13484
|
def,
|
|
13231
13485
|
hookType,
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
def,
|
|
13236
|
-
hookType,
|
|
13237
|
-
converterConfig
|
|
13238
|
-
}),
|
|
13239
|
-
...emitArrayPassthroughFields({
|
|
13240
|
-
def,
|
|
13241
|
-
hookType,
|
|
13242
|
-
converterConfig
|
|
13486
|
+
eventName,
|
|
13487
|
+
converterConfig,
|
|
13488
|
+
warn
|
|
13243
13489
|
}),
|
|
13244
13490
|
type: hookType,
|
|
13245
13491
|
...command !== void 0 && command !== null && { command },
|
|
@@ -13257,6 +13503,17 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13257
13503
|
return hooks;
|
|
13258
13504
|
}
|
|
13259
13505
|
/**
|
|
13506
|
+
* A `warn` that says each distinct thing once per conversion.
|
|
13507
|
+
*/
|
|
13508
|
+
function warnOnce(logger) {
|
|
13509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13510
|
+
return (message) => {
|
|
13511
|
+
if (seen.has(message)) return;
|
|
13512
|
+
seen.add(message);
|
|
13513
|
+
logger?.warn(message);
|
|
13514
|
+
};
|
|
13515
|
+
}
|
|
13516
|
+
/**
|
|
13260
13517
|
* Convert canonical hooks config to tool-specific format (shared by Claude and Factory Droid).
|
|
13261
13518
|
* Uses explicit event name mapping tables rather than algorithmic case conversion,
|
|
13262
13519
|
* since tool event names may differ entirely from canonical names
|
|
@@ -13268,17 +13525,23 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
13268
13525
|
toolOverrideHooks,
|
|
13269
13526
|
supportedEvents: converterConfig.supportedEvents
|
|
13270
13527
|
});
|
|
13528
|
+
const warn = warnOnce(logger);
|
|
13271
13529
|
const result = {};
|
|
13272
13530
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
13273
13531
|
const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
|
|
13274
|
-
const byMatcher = groupDefinitionsByMatcher(
|
|
13532
|
+
const byMatcher = groupDefinitionsByMatcher({
|
|
13533
|
+
definitions,
|
|
13534
|
+
converterConfig
|
|
13535
|
+
});
|
|
13275
13536
|
const entries = [];
|
|
13276
13537
|
const isNoMatcherEvent = converterConfig.noMatcherEvents?.has(eventName) ?? false;
|
|
13277
|
-
for (const
|
|
13538
|
+
for (const { matcher: matcherKey, defs } of byMatcher.values()) {
|
|
13278
13539
|
if (isNoMatcherEvent && matcherKey) logger?.warn(`matcher "${matcherKey}" on "${eventName}" hook will be ignored — this event does not support matchers`);
|
|
13279
13540
|
const hooks = buildToolHooks({
|
|
13280
13541
|
defs,
|
|
13281
|
-
|
|
13542
|
+
eventName,
|
|
13543
|
+
converterConfig,
|
|
13544
|
+
warn
|
|
13282
13545
|
});
|
|
13283
13546
|
if (hooks.length === 0) continue;
|
|
13284
13547
|
const includeMatcher = matcherKey && !isNoMatcherEvent;
|
|
@@ -13354,9 +13617,27 @@ function isStringArray(value) {
|
|
|
13354
13617
|
}
|
|
13355
13618
|
/** Compare object values without letting key order decide the answer. */
|
|
13356
13619
|
function stableJson(value) {
|
|
13620
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
13357
13621
|
return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
|
|
13358
13622
|
}
|
|
13359
13623
|
/**
|
|
13624
|
+
* A string map safe to hand a tool as a hook's environment block. On top of
|
|
13625
|
+
* {@link isStringRecord} it rejects a non-plain object (a class instance is not
|
|
13626
|
+
* data) and applies the control-character rule to the values, as
|
|
13627
|
+
* {@link isSafeStringArray} does for `args`.
|
|
13628
|
+
*
|
|
13629
|
+
* The keys are checked more strictly than the values. A tool builds each entry
|
|
13630
|
+
* back into a `KEY=VALUE` string for the spawned process, so a key holding `=`
|
|
13631
|
+
* (or a control character, or nothing at all) names a different variable than
|
|
13632
|
+
* it appears to — `PATH=/tmp/evil` written as a key would set `PATH`. An
|
|
13633
|
+
* authored `.rulesync/hooks.*` can arrive via `rulesync fetch`, so that is not
|
|
13634
|
+
* a shape to pass along.
|
|
13635
|
+
*/
|
|
13636
|
+
function isSafeStringRecord(value) {
|
|
13637
|
+
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
13638
|
+
return Object.entries(value).every(([key, entry]) => key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || entry.includes(char)));
|
|
13639
|
+
}
|
|
13640
|
+
/**
|
|
13360
13641
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
13361
13642
|
* field the schema guards with `safeString`, or the next generate fails
|
|
13362
13643
|
* validation on a file this import itself wrote — and the hooks feature is
|
|
@@ -13366,34 +13647,152 @@ function isSafeStringArray(value) {
|
|
|
13366
13647
|
return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
|
|
13367
13648
|
}
|
|
13368
13649
|
/**
|
|
13650
|
+
* A raw string kept only if the canonical field it would land in accepts it,
|
|
13651
|
+
* warning when it does not. The canonical string fields are guarded by
|
|
13652
|
+
* `safeString`, so an existing tool config carrying a control character would
|
|
13653
|
+
* otherwise be imported into a file the next generate refuses to read.
|
|
13654
|
+
*/
|
|
13655
|
+
function importCanonicalString({ value, canonical, warn }) {
|
|
13656
|
+
if (typeof value !== "string") return;
|
|
13657
|
+
if (satisfiesCanonicalField({
|
|
13658
|
+
value,
|
|
13659
|
+
canonical
|
|
13660
|
+
})) return value;
|
|
13661
|
+
warn?.(describeInvalidScalar({
|
|
13662
|
+
tool: canonical,
|
|
13663
|
+
canonical,
|
|
13664
|
+
value
|
|
13665
|
+
}));
|
|
13666
|
+
}
|
|
13667
|
+
/**
|
|
13369
13668
|
* Import the payload fields specific to a hook type, type-checking each raw
|
|
13370
13669
|
* value before it enters the canonical definition.
|
|
13371
13670
|
*/
|
|
13372
|
-
function importTypePayloadFields({ h, hookType }) {
|
|
13373
|
-
if (hookType === "http")
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13671
|
+
function importTypePayloadFields({ h, hookType, warn }) {
|
|
13672
|
+
if (hookType === "http") {
|
|
13673
|
+
const url = importCanonicalString({
|
|
13674
|
+
value: h.url,
|
|
13675
|
+
canonical: "url",
|
|
13676
|
+
warn
|
|
13677
|
+
});
|
|
13678
|
+
const headers = isStringRecord(h.headers) && !satisfiesCanonicalField({
|
|
13679
|
+
value: h.headers,
|
|
13680
|
+
canonical: "headers"
|
|
13681
|
+
}) ? (warn?.(describeInvalidScalar({
|
|
13682
|
+
tool: "headers",
|
|
13683
|
+
canonical: "headers",
|
|
13684
|
+
value: h.headers
|
|
13685
|
+
})), void 0) : h.headers;
|
|
13686
|
+
return {
|
|
13687
|
+
...url !== void 0 && { url },
|
|
13688
|
+
...isStringRecord(headers) && { headers },
|
|
13689
|
+
...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
|
|
13690
|
+
};
|
|
13691
|
+
}
|
|
13692
|
+
if (hookType === "mcp_tool") {
|
|
13693
|
+
const server = importCanonicalString({
|
|
13694
|
+
value: h.server,
|
|
13695
|
+
canonical: "server",
|
|
13696
|
+
warn
|
|
13697
|
+
});
|
|
13698
|
+
const tool = importCanonicalString({
|
|
13699
|
+
value: h.tool,
|
|
13700
|
+
canonical: "tool",
|
|
13701
|
+
warn
|
|
13702
|
+
});
|
|
13703
|
+
return {
|
|
13704
|
+
...server !== void 0 && { server },
|
|
13705
|
+
...tool !== void 0 && { tool },
|
|
13706
|
+
...h.input !== null && typeof h.input === "object" && !Array.isArray(h.input) && { input: h.input }
|
|
13707
|
+
};
|
|
13708
|
+
}
|
|
13709
|
+
if (hookType === "prompt" || hookType === "agent") {
|
|
13710
|
+
const model = importCanonicalString({
|
|
13711
|
+
value: h.model,
|
|
13712
|
+
canonical: "model",
|
|
13713
|
+
warn
|
|
13714
|
+
});
|
|
13715
|
+
return model !== void 0 ? { model } : {};
|
|
13716
|
+
}
|
|
13384
13717
|
return {};
|
|
13385
13718
|
}
|
|
13386
13719
|
/**
|
|
13720
|
+
* Import every per-hook passthrough kind for one tool hook record, reversing
|
|
13721
|
+
* the emit side in {@link buildToolHooks}.
|
|
13722
|
+
*/
|
|
13723
|
+
function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
|
|
13724
|
+
return {
|
|
13725
|
+
...importPassthroughFields({
|
|
13726
|
+
h,
|
|
13727
|
+
hookType,
|
|
13728
|
+
fields: converterConfig.booleanPassthroughFields ?? [],
|
|
13729
|
+
isValid: isBooleanValue,
|
|
13730
|
+
warn
|
|
13731
|
+
}),
|
|
13732
|
+
...importPassthroughFields({
|
|
13733
|
+
h,
|
|
13734
|
+
hookType,
|
|
13735
|
+
fields: converterConfig.numberPassthroughFields ?? [],
|
|
13736
|
+
isValid: isImportableNumber,
|
|
13737
|
+
describeInvalid: describeInvalidScalar,
|
|
13738
|
+
warn
|
|
13739
|
+
}),
|
|
13740
|
+
...importPassthroughFields({
|
|
13741
|
+
h,
|
|
13742
|
+
hookType,
|
|
13743
|
+
fields: converterConfig.stringPassthroughFields ?? [],
|
|
13744
|
+
isValid: isImportableString,
|
|
13745
|
+
describeInvalid: describeInvalidScalar,
|
|
13746
|
+
warn
|
|
13747
|
+
}),
|
|
13748
|
+
...importPassthroughFields({
|
|
13749
|
+
h,
|
|
13750
|
+
hookType,
|
|
13751
|
+
fields: converterConfig.arrayPassthroughFields ?? [],
|
|
13752
|
+
isValid: isImportableArray,
|
|
13753
|
+
describeInvalid: describeInvalidArray,
|
|
13754
|
+
warn
|
|
13755
|
+
}),
|
|
13756
|
+
...importPassthroughFields({
|
|
13757
|
+
h,
|
|
13758
|
+
hookType,
|
|
13759
|
+
fields: converterConfig.recordPassthroughFields ?? [],
|
|
13760
|
+
isValid: isEmittableRecord,
|
|
13761
|
+
describeInvalid: describeInvalidRecord,
|
|
13762
|
+
warn
|
|
13763
|
+
})
|
|
13764
|
+
};
|
|
13765
|
+
}
|
|
13766
|
+
/**
|
|
13387
13767
|
* Convert a single tool hook record into a canonical hook definition.
|
|
13388
13768
|
*/
|
|
13389
|
-
function toolHookToCanonical({ h, rawEntry, converterConfig,
|
|
13390
|
-
const command = stripCommandPrefix({
|
|
13391
|
-
command: h.command,
|
|
13392
|
-
converterConfig
|
|
13393
|
-
});
|
|
13769
|
+
function toolHookToCanonical({ h, rawEntry, converterConfig, warn }) {
|
|
13394
13770
|
const hookType = isImportedHookType(h.type) ? h.type : "command";
|
|
13771
|
+
const command = importCanonicalString({
|
|
13772
|
+
value: stripCommandPrefix({
|
|
13773
|
+
command: h.command,
|
|
13774
|
+
converterConfig
|
|
13775
|
+
}),
|
|
13776
|
+
canonical: "command",
|
|
13777
|
+
warn
|
|
13778
|
+
});
|
|
13395
13779
|
const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
|
|
13396
|
-
const prompt =
|
|
13780
|
+
const prompt = importCanonicalString({
|
|
13781
|
+
value: h.prompt,
|
|
13782
|
+
canonical: "prompt",
|
|
13783
|
+
warn
|
|
13784
|
+
});
|
|
13785
|
+
const name = importCanonicalString({
|
|
13786
|
+
value: h.name,
|
|
13787
|
+
canonical: "name",
|
|
13788
|
+
warn
|
|
13789
|
+
});
|
|
13790
|
+
const description = importCanonicalString({
|
|
13791
|
+
value: h.description,
|
|
13792
|
+
canonical: "description",
|
|
13793
|
+
warn
|
|
13794
|
+
});
|
|
13795
|
+
const matcher = rawEntry.matcher;
|
|
13397
13796
|
return {
|
|
13398
13797
|
type: hookType,
|
|
13399
13798
|
...command !== void 0 && command !== null && { command },
|
|
@@ -13401,40 +13800,130 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
|
|
|
13401
13800
|
...prompt !== void 0 && prompt !== null && { prompt },
|
|
13402
13801
|
...importTypePayloadFields({
|
|
13403
13802
|
h,
|
|
13404
|
-
hookType
|
|
13803
|
+
hookType,
|
|
13804
|
+
warn
|
|
13405
13805
|
}),
|
|
13406
|
-
...converterConfig.passthroughFields?.includes("name") &&
|
|
13407
|
-
...converterConfig.passthroughFields?.includes("description") &&
|
|
13408
|
-
...
|
|
13409
|
-
h,
|
|
13410
|
-
converterConfig
|
|
13411
|
-
}),
|
|
13412
|
-
...importStringPassthroughFields({
|
|
13413
|
-
h,
|
|
13414
|
-
converterConfig
|
|
13415
|
-
}),
|
|
13416
|
-
...importArrayPassthroughFields({
|
|
13806
|
+
...converterConfig.passthroughFields?.includes("name") && name !== void 0 && { name },
|
|
13807
|
+
...converterConfig.passthroughFields?.includes("description") && description !== void 0 && { description },
|
|
13808
|
+
...importAllPassthroughFields({
|
|
13417
13809
|
h,
|
|
13810
|
+
hookType,
|
|
13418
13811
|
converterConfig,
|
|
13419
|
-
|
|
13812
|
+
warn
|
|
13420
13813
|
}),
|
|
13421
13814
|
...importGroupPassthroughFields({
|
|
13422
13815
|
rawEntry,
|
|
13423
13816
|
converterConfig
|
|
13424
13817
|
}),
|
|
13425
|
-
...
|
|
13818
|
+
...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
|
|
13426
13819
|
};
|
|
13427
13820
|
}
|
|
13428
13821
|
/**
|
|
13429
|
-
*
|
|
13822
|
+
* The fields whose value decides what a hook *is*, listed per hook type. When
|
|
13823
|
+
* one of them cannot be imported, dropping just the field would leave
|
|
13824
|
+
* something worse than nothing: a hook that loses its body runs nothing, and
|
|
13825
|
+
* one that loses its `matcher` fires on *everything* — a silent widening of
|
|
13826
|
+
* what the imported rule does. So the whole definition is skipped instead,
|
|
13827
|
+
* with the reason named. A field that does not define *this* type (a `prompt`
|
|
13828
|
+
* left on a command hook) is not one of them: it is dropped on its own, the
|
|
13829
|
+
* way any other unusable field is.
|
|
13830
|
+
*/
|
|
13831
|
+
function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
13832
|
+
const fields = [{
|
|
13833
|
+
field: "matcher",
|
|
13834
|
+
value: rawEntry.matcher
|
|
13835
|
+
}];
|
|
13836
|
+
if (hookType === "command") fields.push({
|
|
13837
|
+
field: "command",
|
|
13838
|
+
value: typeof h.command === "string" ? stripCommandPrefix({
|
|
13839
|
+
command: h.command,
|
|
13840
|
+
converterConfig
|
|
13841
|
+
}) : h.command
|
|
13842
|
+
});
|
|
13843
|
+
if (hookType === "prompt" || hookType === "agent") fields.push({
|
|
13844
|
+
field: "prompt",
|
|
13845
|
+
value: h.prompt
|
|
13846
|
+
});
|
|
13847
|
+
if (hookType === "http") fields.push({
|
|
13848
|
+
field: "url",
|
|
13849
|
+
value: h.url
|
|
13850
|
+
});
|
|
13851
|
+
if (hookType === "mcp_tool") fields.push({
|
|
13852
|
+
field: "server",
|
|
13853
|
+
value: h.server
|
|
13854
|
+
}, {
|
|
13855
|
+
field: "tool",
|
|
13856
|
+
value: h.tool
|
|
13857
|
+
});
|
|
13858
|
+
return fields;
|
|
13859
|
+
}
|
|
13860
|
+
/**
|
|
13861
|
+
* Why no hook of this matcher group can be imported, or `undefined` when they
|
|
13862
|
+
* can. A group field that *restricts* when its hooks run (`subdividesGroup`)
|
|
13863
|
+
* is the group-level twin of `matcher`: importing the group without it would
|
|
13864
|
+
* widen every hook in it, so the group is skipped instead.
|
|
13430
13865
|
*/
|
|
13431
|
-
function
|
|
13432
|
-
|
|
13866
|
+
function describeGroupSkipReason({ rawEntry, converterConfig }) {
|
|
13867
|
+
const entry = rawEntry;
|
|
13868
|
+
for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
|
|
13869
|
+
const value = entry[tool];
|
|
13870
|
+
if (subdividesGroup !== true || value === void 0) continue;
|
|
13871
|
+
if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${JSON.stringify(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
|
|
13872
|
+
}
|
|
13873
|
+
}
|
|
13874
|
+
/**
|
|
13875
|
+
* Why this hook cannot be imported, or `undefined` when it can.
|
|
13876
|
+
*/
|
|
13877
|
+
function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
|
|
13878
|
+
for (const { field, value } of definingFields({
|
|
13433
13879
|
h,
|
|
13434
13880
|
rawEntry,
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
}))
|
|
13881
|
+
hookType,
|
|
13882
|
+
converterConfig
|
|
13883
|
+
})) {
|
|
13884
|
+
if (value === void 0 || satisfiesCanonicalField({
|
|
13885
|
+
value,
|
|
13886
|
+
canonical: field
|
|
13887
|
+
})) continue;
|
|
13888
|
+
return `Skipping a hook while importing: its "${field}" (${JSON.stringify(value)}) is unusable — ${describeScalarConstraint({
|
|
13889
|
+
canonical: field,
|
|
13890
|
+
value
|
|
13891
|
+
})} Keeping the hook without it would change what it does, so the whole hook is skipped.`;
|
|
13892
|
+
}
|
|
13893
|
+
}
|
|
13894
|
+
/**
|
|
13895
|
+
* Convert a single tool matcher entry into canonical hook definitions.
|
|
13896
|
+
*/
|
|
13897
|
+
function toolMatcherEntryToCanonical({ rawEntry, converterConfig, warn }) {
|
|
13898
|
+
const hookDefs = rawEntry.hooks ?? [];
|
|
13899
|
+
const groupSkipReason = describeGroupSkipReason({
|
|
13900
|
+
rawEntry,
|
|
13901
|
+
converterConfig
|
|
13902
|
+
});
|
|
13903
|
+
if (groupSkipReason !== void 0) {
|
|
13904
|
+
warn?.(groupSkipReason);
|
|
13905
|
+
return [];
|
|
13906
|
+
}
|
|
13907
|
+
const definitions = [];
|
|
13908
|
+
for (const h of hookDefs) {
|
|
13909
|
+
const skipReason = describeHookSkipReason({
|
|
13910
|
+
h,
|
|
13911
|
+
rawEntry,
|
|
13912
|
+
hookType: isImportedHookType(h.type) ? h.type : "command",
|
|
13913
|
+
converterConfig
|
|
13914
|
+
});
|
|
13915
|
+
if (skipReason !== void 0) {
|
|
13916
|
+
warn?.(skipReason);
|
|
13917
|
+
continue;
|
|
13918
|
+
}
|
|
13919
|
+
definitions.push(toolHookToCanonical({
|
|
13920
|
+
h,
|
|
13921
|
+
rawEntry,
|
|
13922
|
+
converterConfig,
|
|
13923
|
+
warn
|
|
13924
|
+
}));
|
|
13925
|
+
}
|
|
13926
|
+
return definitions;
|
|
13438
13927
|
}
|
|
13439
13928
|
/**
|
|
13440
13929
|
* Assemble the canonical hooks config a tool importer writes to
|
|
@@ -13464,6 +13953,7 @@ function buildImportedHooksConfig({ hooks, overrideKey, version = 1, extraOverri
|
|
|
13464
13953
|
}
|
|
13465
13954
|
function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
13466
13955
|
if (hooks === null || hooks === void 0 || typeof hooks !== "object") return {};
|
|
13956
|
+
const warn = warnOnce(logger);
|
|
13467
13957
|
const canonical = {};
|
|
13468
13958
|
for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
|
|
13469
13959
|
const eventName = converterConfig.toolToCanonicalEventNames[toolEventName] ?? toolEventName;
|
|
@@ -13474,7 +13964,7 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
13474
13964
|
defs.push(...toolMatcherEntryToCanonical({
|
|
13475
13965
|
rawEntry,
|
|
13476
13966
|
converterConfig,
|
|
13477
|
-
|
|
13967
|
+
warn
|
|
13478
13968
|
}));
|
|
13479
13969
|
}
|
|
13480
13970
|
if (defs.length > 0) canonical[eventName] = defs;
|
|
@@ -13513,7 +14003,9 @@ const ANTIGRAVITY_HOOK_NAME = "rulesync";
|
|
|
13513
14003
|
* map for import. Accepts both the documented named-hook shape
|
|
13514
14004
|
* (`{ "<name>": { "<Event>": [...], "enabled"?: bool } }`) and a legacy flat
|
|
13515
14005
|
* shape (`{ "<Event>": [...] }`) so older or hand-written files still import.
|
|
13516
|
-
* The per-hook `enabled` flag is ignored
|
|
14006
|
+
* The per-hook `enabled` flag is ignored. The canonical `enabled` field is a
|
|
14007
|
+
* property of a single hook definition, whereas Antigravity's flag gates a whole
|
|
14008
|
+
* named group, so the two do not map onto each other.
|
|
13517
14009
|
*/
|
|
13518
14010
|
function flattenAntigravityHooks(parsed) {
|
|
13519
14011
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -13595,7 +14087,7 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
13595
14087
|
validate
|
|
13596
14088
|
});
|
|
13597
14089
|
}
|
|
13598
|
-
toRulesyncHooks() {
|
|
14090
|
+
toRulesyncHooks({ logger } = {}) {
|
|
13599
14091
|
let parsed;
|
|
13600
14092
|
try {
|
|
13601
14093
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -13604,7 +14096,8 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
13604
14096
|
}
|
|
13605
14097
|
const hooks = toolHooksToCanonical({
|
|
13606
14098
|
hooks: flattenAntigravityHooks(parsed),
|
|
13607
|
-
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG
|
|
14099
|
+
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
|
|
14100
|
+
logger
|
|
13608
14101
|
});
|
|
13609
14102
|
const overrideKey = this.constructor.getOverrideKey();
|
|
13610
14103
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
@@ -13935,6 +14428,14 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13935
14428
|
isDeletable() {
|
|
13936
14429
|
return false;
|
|
13937
14430
|
}
|
|
14431
|
+
/**
|
|
14432
|
+
* The converter config used for both directions. Exposed as a static hook so
|
|
14433
|
+
* plugin-scoped subclasses can swap tool-specific details (e.g. the project
|
|
14434
|
+
* directory variable) without duplicating the rest of the config.
|
|
14435
|
+
*/
|
|
14436
|
+
static getConverterConfig() {
|
|
14437
|
+
return CLAUDE_CONVERTER_CONFIG;
|
|
14438
|
+
}
|
|
13938
14439
|
static getSettablePaths(_options = {}) {
|
|
13939
14440
|
return {
|
|
13940
14441
|
relativeDirPath: CLAUDECODE_DIR,
|
|
@@ -13960,7 +14461,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13960
14461
|
const claudeHooks = canonicalToToolHooks({
|
|
13961
14462
|
config,
|
|
13962
14463
|
toolOverrideHooks: config.claudecode?.hooks,
|
|
13963
|
-
converterConfig:
|
|
14464
|
+
converterConfig: this.getConverterConfig(),
|
|
13964
14465
|
logger
|
|
13965
14466
|
});
|
|
13966
14467
|
const fileContent = applySharedConfigPatch({
|
|
@@ -13978,7 +14479,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13978
14479
|
validate
|
|
13979
14480
|
});
|
|
13980
14481
|
}
|
|
13981
|
-
toRulesyncHooks() {
|
|
14482
|
+
toRulesyncHooks({ logger } = {}) {
|
|
13982
14483
|
let settings;
|
|
13983
14484
|
try {
|
|
13984
14485
|
settings = JSON.parse(this.getFileContent());
|
|
@@ -13987,7 +14488,8 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13987
14488
|
}
|
|
13988
14489
|
const hooks = toolHooksToCanonical({
|
|
13989
14490
|
hooks: settings.hooks,
|
|
13990
|
-
converterConfig:
|
|
14491
|
+
converterConfig: this.constructor.getConverterConfig(),
|
|
14492
|
+
logger
|
|
13991
14493
|
});
|
|
13992
14494
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
13993
14495
|
hooks,
|
|
@@ -14016,6 +14518,21 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
|
14016
14518
|
isDeletable() {
|
|
14017
14519
|
return true;
|
|
14018
14520
|
}
|
|
14521
|
+
/**
|
|
14522
|
+
* Plugin hook scripts ship inside the plugin, so their commands must resolve
|
|
14523
|
+
* against the plugin install directory rather than the consumer's project
|
|
14524
|
+
* root. Upstream documents `"${CLAUDE_PLUGIN_ROOT}"/scripts/format-code.sh`;
|
|
14525
|
+
* `$CLAUDE_PROJECT_DIR` would expand to a path in the consumer's own repo,
|
|
14526
|
+
* where the bundled script does not exist.
|
|
14527
|
+
*
|
|
14528
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
14529
|
+
*/
|
|
14530
|
+
static getConverterConfig() {
|
|
14531
|
+
return {
|
|
14532
|
+
...super.getConverterConfig(),
|
|
14533
|
+
projectDirVar: "$CLAUDE_PLUGIN_ROOT"
|
|
14534
|
+
};
|
|
14535
|
+
}
|
|
14019
14536
|
static getSettablePaths() {
|
|
14020
14537
|
return {
|
|
14021
14538
|
relativeDirPath: CLAUDECODE_PLUGIN_HOOKS_DIR,
|
|
@@ -14038,6 +14555,10 @@ const CODEXCLI_CONVERTER_CONFIG = {
|
|
|
14038
14555
|
}, {
|
|
14039
14556
|
canonical: "statusMessage",
|
|
14040
14557
|
tool: "statusMessage"
|
|
14558
|
+
}],
|
|
14559
|
+
numberPassthroughFields: [{
|
|
14560
|
+
canonical: "additionalContextLimit",
|
|
14561
|
+
tool: "additionalContextLimit"
|
|
14041
14562
|
}]
|
|
14042
14563
|
};
|
|
14043
14564
|
/**
|
|
@@ -14127,13 +14648,14 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14127
14648
|
validate
|
|
14128
14649
|
});
|
|
14129
14650
|
}
|
|
14130
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
14651
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
14131
14652
|
const paths = CodexcliHooks.getSettablePaths({ global });
|
|
14132
14653
|
const config = rulesyncHooks.getJson();
|
|
14133
14654
|
const codexHooks = canonicalToToolHooks({
|
|
14134
14655
|
config,
|
|
14135
14656
|
toolOverrideHooks: config.codexcli?.hooks,
|
|
14136
|
-
converterConfig: CODEXCLI_CONVERTER_CONFIG
|
|
14657
|
+
converterConfig: CODEXCLI_CONVERTER_CONFIG,
|
|
14658
|
+
logger
|
|
14137
14659
|
});
|
|
14138
14660
|
const fileContent = JSON.stringify({ hooks: codexHooks }, null, 2);
|
|
14139
14661
|
return new CodexcliHooks({
|
|
@@ -14144,7 +14666,7 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14144
14666
|
validate
|
|
14145
14667
|
});
|
|
14146
14668
|
}
|
|
14147
|
-
toRulesyncHooks() {
|
|
14669
|
+
toRulesyncHooks({ logger } = {}) {
|
|
14148
14670
|
let parsed;
|
|
14149
14671
|
try {
|
|
14150
14672
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -14153,7 +14675,8 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14153
14675
|
}
|
|
14154
14676
|
const hooks = toolHooksToCanonical({
|
|
14155
14677
|
hooks: parsed.hooks,
|
|
14156
|
-
converterConfig: CODEXCLI_CONVERTER_CONFIG
|
|
14678
|
+
converterConfig: CODEXCLI_CONVERTER_CONFIG,
|
|
14679
|
+
logger
|
|
14157
14680
|
});
|
|
14158
14681
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
14159
14682
|
hooks,
|
|
@@ -14392,13 +14915,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
14392
14915
|
* under `.github/hooks/` is picked up automatically when the CLI is
|
|
14393
14916
|
* invoked from the project root.
|
|
14394
14917
|
*
|
|
14395
|
-
* - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` —
|
|
14396
|
-
*
|
|
14397
|
-
*
|
|
14398
|
-
*
|
|
14399
|
-
*
|
|
14400
|
-
*
|
|
14401
|
-
*
|
|
14918
|
+
* - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` — the directory is
|
|
14919
|
+
* the documented user-level hooks location ("`*.json` files in the
|
|
14920
|
+
* user-level hooks directory. By default this is `~/.copilot/hooks/` on
|
|
14921
|
+
* macOS and Linux, or `%USERPROFILE%\.copilot\hooks\` on Windows"). Every
|
|
14922
|
+
* `*.json` in it is loaded, so the filename remains rulesync's choice, as it
|
|
14923
|
+
* is for project scope. `COPILOT_HOME` relocates the directory upstream
|
|
14924
|
+
* (`$COPILOT_HOME/hooks/`); rulesync does not read that variable yet.
|
|
14402
14925
|
*
|
|
14403
14926
|
* Hook entries on the six matcher-aware events (see
|
|
14404
14927
|
* {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
|
|
@@ -14809,17 +15332,18 @@ const DEEPAGENTS_MCP_FILE_NAME = ".mcp.json";
|
|
|
14809
15332
|
const DEEPAGENTS_HOOKS_FILE_NAME = "hooks.json";
|
|
14810
15333
|
//#endregion
|
|
14811
15334
|
//#region src/features/hooks/deepagents-hooks.ts
|
|
14812
|
-
function
|
|
14813
|
-
|
|
14814
|
-
return Array.isArray(val.hooks);
|
|
15335
|
+
function isRecord(value) {
|
|
15336
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14815
15337
|
}
|
|
14816
15338
|
/**
|
|
14817
|
-
* Convert canonical hooks config to deepagents
|
|
15339
|
+
* Convert the canonical hooks config to the deepagents Hooks v2 document.
|
|
14818
15340
|
*
|
|
14819
|
-
*
|
|
14820
|
-
* { "hooks":
|
|
15341
|
+
* ```json
|
|
15342
|
+
* { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "…" }] }] } }
|
|
15343
|
+
* ```
|
|
14821
15344
|
*
|
|
14822
|
-
*
|
|
15345
|
+
* Definitions sharing an event and matcher land in one group, preserving their
|
|
15346
|
+
* authored order — upstream runs a group's handlers in sequence.
|
|
14823
15347
|
*/
|
|
14824
15348
|
function canonicalToDeepagentsHooks(config) {
|
|
14825
15349
|
const supported = new Set(DEEPAGENTS_HOOK_EVENTS);
|
|
@@ -14827,7 +15351,7 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
14827
15351
|
...config.hooks,
|
|
14828
15352
|
...config.deepagents?.hooks
|
|
14829
15353
|
};
|
|
14830
|
-
const
|
|
15354
|
+
const hooks = {};
|
|
14831
15355
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
14832
15356
|
if (!supported.has(canonicalEvent)) continue;
|
|
14833
15357
|
const deepagentsEvent = CANONICAL_TO_DEEPAGENTS_EVENT_NAMES[canonicalEvent];
|
|
@@ -14835,43 +15359,69 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
14835
15359
|
for (const def of definitions) {
|
|
14836
15360
|
if ((def.type ?? "command") !== "command") continue;
|
|
14837
15361
|
if (!def.command) continue;
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
command:
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
15362
|
+
const handler = {
|
|
15363
|
+
type: "command",
|
|
15364
|
+
command: def.command
|
|
15365
|
+
};
|
|
15366
|
+
if (def.timeout !== void 0 && def.timeout !== null && def.timeout > 0) handler.timeout = def.timeout;
|
|
15367
|
+
if (def.statusMessage !== void 0 && def.statusMessage !== null) handler.statusMessage = def.statusMessage;
|
|
15368
|
+
const matcher = def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" ? def.matcher : void 0;
|
|
15369
|
+
const groups = hooks[deepagentsEvent] ??= [];
|
|
15370
|
+
const group = groups.find((candidate) => candidate.matcher === matcher);
|
|
15371
|
+
if (group) group.hooks.push(handler);
|
|
15372
|
+
else groups.push({
|
|
15373
|
+
...matcher !== void 0 && { matcher },
|
|
15374
|
+
hooks: [handler]
|
|
14846
15375
|
});
|
|
14847
15376
|
}
|
|
14848
15377
|
}
|
|
14849
|
-
return
|
|
15378
|
+
return hooks;
|
|
15379
|
+
}
|
|
15380
|
+
/**
|
|
15381
|
+
* Convert the Hooks v2 document back to the canonical hooks record.
|
|
15382
|
+
*/
|
|
15383
|
+
function deepagentsToCanonicalHooks(hooks) {
|
|
15384
|
+
const canonical = {};
|
|
15385
|
+
for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
|
|
15386
|
+
const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
|
|
15387
|
+
if (!canonicalEvent || !Array.isArray(groups)) continue;
|
|
15388
|
+
for (const group of groups) {
|
|
15389
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
15390
|
+
for (const handler of group.hooks) {
|
|
15391
|
+
if (!isRecord(handler) || typeof handler.command !== "string") continue;
|
|
15392
|
+
const def = {
|
|
15393
|
+
type: "command",
|
|
15394
|
+
command: handler.command
|
|
15395
|
+
};
|
|
15396
|
+
if (typeof group.matcher === "string" && group.matcher !== "") def.matcher = group.matcher;
|
|
15397
|
+
if (typeof handler.timeout === "number") def.timeout = handler.timeout;
|
|
15398
|
+
if (typeof handler.statusMessage === "string") def.statusMessage = handler.statusMessage;
|
|
15399
|
+
(canonical[canonicalEvent] ??= []).push(def);
|
|
15400
|
+
}
|
|
15401
|
+
}
|
|
15402
|
+
}
|
|
15403
|
+
return canonical;
|
|
14850
15404
|
}
|
|
14851
15405
|
/**
|
|
14852
|
-
*
|
|
15406
|
+
* Read the pre-v2 flat list. deepagents still loads it until 2026-09-01, so a
|
|
15407
|
+
* `hooks.json` a user has not migrated yet is imported rather than discarded —
|
|
15408
|
+
* but rulesync only ever writes the v2 shape, so regenerating migrates it.
|
|
14853
15409
|
*/
|
|
14854
|
-
function
|
|
15410
|
+
function deepagentsLegacyToCanonicalHooks(entries) {
|
|
14855
15411
|
const canonical = {};
|
|
14856
|
-
for (const entry of
|
|
14857
|
-
if (
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
const
|
|
14863
|
-
|
|
14864
|
-
const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
|
|
15412
|
+
for (const entry of entries) {
|
|
15413
|
+
if (!isRecord(entry)) continue;
|
|
15414
|
+
const argv = entry.command;
|
|
15415
|
+
if (!Array.isArray(argv) || argv.length === 0) continue;
|
|
15416
|
+
const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
|
|
15417
|
+
const events = Array.isArray(entry.events) ? entry.events : [];
|
|
15418
|
+
for (const legacyEvent of events) {
|
|
15419
|
+
const canonicalEvent = typeof legacyEvent === "string" ? DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES[legacyEvent] : void 0;
|
|
14865
15420
|
if (!canonicalEvent) continue;
|
|
14866
|
-
|
|
14867
|
-
if (existing) existing.push({
|
|
15421
|
+
(canonical[canonicalEvent] ??= []).push({
|
|
14868
15422
|
type: "command",
|
|
14869
15423
|
command
|
|
14870
15424
|
});
|
|
14871
|
-
else canonical[canonicalEvent] = [{
|
|
14872
|
-
type: "command",
|
|
14873
|
-
command
|
|
14874
|
-
}];
|
|
14875
15425
|
}
|
|
14876
15426
|
}
|
|
14877
15427
|
return canonical;
|
|
@@ -14880,7 +15430,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14880
15430
|
constructor(params) {
|
|
14881
15431
|
super({
|
|
14882
15432
|
...params,
|
|
14883
|
-
fileContent: params.fileContent ?? JSON.stringify({ hooks:
|
|
15433
|
+
fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
|
|
14884
15434
|
});
|
|
14885
15435
|
}
|
|
14886
15436
|
isDeletable() {
|
|
@@ -14894,7 +15444,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14894
15444
|
}
|
|
14895
15445
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14896
15446
|
const paths = DeepagentsHooks.getSettablePaths({ global });
|
|
14897
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks:
|
|
15447
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} }, null, 2);
|
|
14898
15448
|
return new DeepagentsHooks({
|
|
14899
15449
|
outputRoot,
|
|
14900
15450
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -14922,7 +15472,8 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14922
15472
|
} catch (error) {
|
|
14923
15473
|
throw new Error(`Failed to parse deepagents hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
14924
15474
|
}
|
|
14925
|
-
const
|
|
15475
|
+
const rawHooks = isRecord(parsed) ? parsed.hooks : void 0;
|
|
15476
|
+
const hooks = Array.isArray(rawHooks) ? deepagentsLegacyToCanonicalHooks(rawHooks) : isRecord(rawHooks) ? deepagentsToCanonicalHooks(rawHooks) : {};
|
|
14926
15477
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
14927
15478
|
hooks,
|
|
14928
15479
|
overrideKey: "deepagents"
|
|
@@ -14939,7 +15490,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14939
15490
|
outputRoot,
|
|
14940
15491
|
relativeDirPath,
|
|
14941
15492
|
relativeFilePath,
|
|
14942
|
-
fileContent: JSON.stringify({ hooks:
|
|
15493
|
+
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
14943
15494
|
validate: false
|
|
14944
15495
|
});
|
|
14945
15496
|
}
|
|
@@ -15041,7 +15592,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
15041
15592
|
validate
|
|
15042
15593
|
});
|
|
15043
15594
|
}
|
|
15044
|
-
toRulesyncHooks() {
|
|
15595
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15045
15596
|
let parsed;
|
|
15046
15597
|
try {
|
|
15047
15598
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -15049,8 +15600,9 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
15049
15600
|
throw new Error(`Failed to parse Devin hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
15050
15601
|
}
|
|
15051
15602
|
const hooks = toolHooksToCanonical({
|
|
15052
|
-
hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
15053
|
-
converterConfig: DEVIN_CONVERTER_CONFIG
|
|
15603
|
+
hooks: this.getRelativeFilePath() === "config.json" ? isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
15604
|
+
converterConfig: DEVIN_CONVERTER_CONFIG,
|
|
15605
|
+
logger
|
|
15054
15606
|
});
|
|
15055
15607
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15056
15608
|
hooks,
|
|
@@ -15081,7 +15633,13 @@ const FACTORYDROID_CONVERTER_CONFIG = {
|
|
|
15081
15633
|
toolToCanonicalEventNames: FACTORYDROID_TO_CANONICAL_EVENT_NAMES,
|
|
15082
15634
|
projectDirVar: "$FACTORY_PROJECT_DIR",
|
|
15083
15635
|
prefixDotRelativeCommandsOnly: true,
|
|
15084
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15636
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15637
|
+
groupPassthroughFields: [{
|
|
15638
|
+
canonical: "commandRegex",
|
|
15639
|
+
tool: "commandRegex",
|
|
15640
|
+
valueType: "string",
|
|
15641
|
+
subdividesGroup: true
|
|
15642
|
+
}]
|
|
15085
15643
|
};
|
|
15086
15644
|
var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
15087
15645
|
constructor(params) {
|
|
@@ -15138,7 +15696,7 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
15138
15696
|
validate
|
|
15139
15697
|
});
|
|
15140
15698
|
}
|
|
15141
|
-
toRulesyncHooks() {
|
|
15699
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15142
15700
|
let settings;
|
|
15143
15701
|
try {
|
|
15144
15702
|
settings = JSON.parse(this.getFileContent());
|
|
@@ -15147,7 +15705,8 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
15147
15705
|
}
|
|
15148
15706
|
const hooks = toolHooksToCanonical({
|
|
15149
15707
|
hooks: settings.hooks,
|
|
15150
|
-
converterConfig: FACTORYDROID_CONVERTER_CONFIG
|
|
15708
|
+
converterConfig: FACTORYDROID_CONVERTER_CONFIG,
|
|
15709
|
+
logger
|
|
15151
15710
|
});
|
|
15152
15711
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15153
15712
|
hooks,
|
|
@@ -15177,7 +15736,8 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
15177
15736
|
canonicalToToolEventNames: CANONICAL_TO_GOOSE_EVENT_NAMES,
|
|
15178
15737
|
toolToCanonicalEventNames: GOOSE_TO_CANONICAL_EVENT_NAMES,
|
|
15179
15738
|
projectDirVar: "",
|
|
15180
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15739
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15740
|
+
wildcardMatcherMeansAll: true
|
|
15181
15741
|
};
|
|
15182
15742
|
/**
|
|
15183
15743
|
* Represents a Goose lifecycle hooks file.
|
|
@@ -15215,13 +15775,14 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
15215
15775
|
validate
|
|
15216
15776
|
});
|
|
15217
15777
|
}
|
|
15218
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
15778
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
15219
15779
|
const paths = GooseHooks.getSettablePaths({ global });
|
|
15220
15780
|
const config = rulesyncHooks.getJson();
|
|
15221
15781
|
const gooseHooks = canonicalToToolHooks({
|
|
15222
15782
|
config,
|
|
15223
15783
|
toolOverrideHooks: config.goose?.hooks,
|
|
15224
|
-
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
15784
|
+
converterConfig: GOOSE_CONVERTER_CONFIG,
|
|
15785
|
+
logger
|
|
15225
15786
|
});
|
|
15226
15787
|
const fileContent = JSON.stringify({ hooks: gooseHooks }, null, 2);
|
|
15227
15788
|
return new GooseHooks({
|
|
@@ -15232,7 +15793,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
15232
15793
|
validate
|
|
15233
15794
|
});
|
|
15234
15795
|
}
|
|
15235
|
-
toRulesyncHooks() {
|
|
15796
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15236
15797
|
let parsed;
|
|
15237
15798
|
try {
|
|
15238
15799
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -15241,7 +15802,8 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
15241
15802
|
}
|
|
15242
15803
|
const hooks = toolHooksToCanonical({
|
|
15243
15804
|
hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
|
|
15244
|
-
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
15805
|
+
converterConfig: GOOSE_CONVERTER_CONFIG,
|
|
15806
|
+
logger
|
|
15245
15807
|
});
|
|
15246
15808
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15247
15809
|
hooks,
|
|
@@ -15272,6 +15834,11 @@ const GROKCLI_CONVERTER_CONFIG = {
|
|
|
15272
15834
|
toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
|
|
15273
15835
|
projectDirVar: "",
|
|
15274
15836
|
supportedHookTypes: /* @__PURE__ */ new Set(["command", "http"]),
|
|
15837
|
+
recordPassthroughFields: [{
|
|
15838
|
+
canonical: "env",
|
|
15839
|
+
tool: "env",
|
|
15840
|
+
commandOnly: true
|
|
15841
|
+
}],
|
|
15275
15842
|
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
15276
15843
|
"sessionStart",
|
|
15277
15844
|
"sessionEnd",
|
|
@@ -15340,7 +15907,7 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
|
|
|
15340
15907
|
validate
|
|
15341
15908
|
});
|
|
15342
15909
|
}
|
|
15343
|
-
toRulesyncHooks() {
|
|
15910
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15344
15911
|
let parsed;
|
|
15345
15912
|
try {
|
|
15346
15913
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -15348,8 +15915,9 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
|
|
|
15348
15915
|
throw new Error(`Failed to parse Grok hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
15349
15916
|
}
|
|
15350
15917
|
const hooks = toolHooksToCanonical({
|
|
15351
|
-
hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
|
|
15352
|
-
converterConfig: GROKCLI_CONVERTER_CONFIG
|
|
15918
|
+
hooks: isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {},
|
|
15919
|
+
converterConfig: GROKCLI_CONVERTER_CONFIG,
|
|
15920
|
+
logger
|
|
15353
15921
|
});
|
|
15354
15922
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15355
15923
|
hooks,
|
|
@@ -15670,7 +16238,7 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
15670
16238
|
validate
|
|
15671
16239
|
});
|
|
15672
16240
|
}
|
|
15673
|
-
toRulesyncHooks() {
|
|
16241
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15674
16242
|
let settings;
|
|
15675
16243
|
try {
|
|
15676
16244
|
settings = JSON.parse(this.getFileContent());
|
|
@@ -15679,7 +16247,8 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
15679
16247
|
}
|
|
15680
16248
|
const hooks = toolHooksToCanonical({
|
|
15681
16249
|
hooks: settings.hooks,
|
|
15682
|
-
converterConfig: JUNIE_CONVERTER_CONFIG
|
|
16250
|
+
converterConfig: JUNIE_CONVERTER_CONFIG,
|
|
16251
|
+
logger
|
|
15683
16252
|
});
|
|
15684
16253
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15685
16254
|
hooks,
|
|
@@ -16417,7 +16986,7 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
|
16417
16986
|
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
16418
16987
|
action,
|
|
16419
16988
|
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
16420
|
-
enabled: true
|
|
16989
|
+
enabled: def.enabled ?? true
|
|
16421
16990
|
});
|
|
16422
16991
|
}
|
|
16423
16992
|
return entries;
|
|
@@ -16457,6 +17026,7 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
16457
17026
|
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
16458
17027
|
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
16459
17028
|
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
17029
|
+
if (entry.enabled === false) def.enabled = false;
|
|
16460
17030
|
(canonical[eventName] ??= []).push(def);
|
|
16461
17031
|
}
|
|
16462
17032
|
return canonical;
|
|
@@ -17401,6 +17971,37 @@ function unsupportedEventNames(params) {
|
|
|
17401
17971
|
const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
|
|
17402
17972
|
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
17403
17973
|
}
|
|
17974
|
+
/**
|
|
17975
|
+
* A logger whose warnings name the tool being converted, matching the rest of
|
|
17976
|
+
* this processor's warnings: the shared hooks converter says what is wrong but
|
|
17977
|
+
* not which tool's file it is reading or writing, and one run walks them all.
|
|
17978
|
+
*
|
|
17979
|
+
* Every method delegates explicitly rather than through a prototype, so the
|
|
17980
|
+
* real logger keeps owning its state — a wrapper that inherited it would
|
|
17981
|
+
* absorb the writes `configure` and `outputJson` make.
|
|
17982
|
+
*/
|
|
17983
|
+
function withToolTargetPrefix({ logger, toolTarget }) {
|
|
17984
|
+
return {
|
|
17985
|
+
configure: (options) => logger.configure(options),
|
|
17986
|
+
get verbose() {
|
|
17987
|
+
return logger.verbose;
|
|
17988
|
+
},
|
|
17989
|
+
get silent() {
|
|
17990
|
+
return logger.silent;
|
|
17991
|
+
},
|
|
17992
|
+
get jsonMode() {
|
|
17993
|
+
return logger.jsonMode;
|
|
17994
|
+
},
|
|
17995
|
+
captureData: (key, value) => logger.captureData(key, value),
|
|
17996
|
+
getJsonData: () => logger.getJsonData(),
|
|
17997
|
+
outputJson: (success, error) => logger.outputJson(success, error),
|
|
17998
|
+
info: (message, ...args) => logger.info(message, ...args),
|
|
17999
|
+
success: (message, ...args) => logger.success(message, ...args),
|
|
18000
|
+
warn: (message, ...args) => logger.warn(`For ${toolTarget}: ${message}`, ...args),
|
|
18001
|
+
error: (message, code, ...args) => logger.error(message, code, ...args),
|
|
18002
|
+
debug: (message, ...args) => logger.debug(message, ...args)
|
|
18003
|
+
};
|
|
18004
|
+
}
|
|
17404
18005
|
function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
|
|
17405
18006
|
if (factory.supportsMatcher && !factory.matcherEvents) return [];
|
|
17406
18007
|
const matcherEvents = factory.matcherEvents ? new Set(factory.matcherEvents) : void 0;
|
|
@@ -17623,13 +18224,13 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
17623
18224
|
["deepagents", {
|
|
17624
18225
|
class: DeepagentsHooks,
|
|
17625
18226
|
meta: {
|
|
17626
|
-
supportsProject:
|
|
18227
|
+
supportsProject: true,
|
|
17627
18228
|
supportsGlobal: true,
|
|
17628
18229
|
supportsImport: true
|
|
17629
18230
|
},
|
|
17630
18231
|
supportedEvents: DEEPAGENTS_HOOK_EVENTS,
|
|
17631
18232
|
supportedHookTypes: ["command"],
|
|
17632
|
-
supportsMatcher:
|
|
18233
|
+
supportsMatcher: true
|
|
17633
18234
|
}],
|
|
17634
18235
|
["kiro", {
|
|
17635
18236
|
class: KiroHooks,
|
|
@@ -17841,6 +18442,15 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
17841
18442
|
}
|
|
17842
18443
|
for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
|
|
17843
18444
|
}
|
|
18445
|
+
if (this.toolTarget !== "kiro-ide") {
|
|
18446
|
+
const skippedEvents = new Set(unsupportedEventNames({
|
|
18447
|
+
factory,
|
|
18448
|
+
sharedHooks,
|
|
18449
|
+
effectiveHooks
|
|
18450
|
+
}));
|
|
18451
|
+
const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
|
|
18452
|
+
if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only kiro-ide supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
|
|
18453
|
+
}
|
|
17844
18454
|
const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
|
|
17845
18455
|
factory,
|
|
17846
18456
|
effectiveHooks
|
|
@@ -17851,7 +18461,10 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
17851
18461
|
rulesyncHooks,
|
|
17852
18462
|
validate: true,
|
|
17853
18463
|
global: this.global,
|
|
17854
|
-
logger:
|
|
18464
|
+
logger: withToolTargetPrefix({
|
|
18465
|
+
logger: this.logger,
|
|
18466
|
+
toolTarget: this.toolTarget
|
|
18467
|
+
})
|
|
17855
18468
|
})];
|
|
17856
18469
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
17857
18470
|
outputRoot: this.outputRoot,
|
|
@@ -17861,7 +18474,12 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
17861
18474
|
return result;
|
|
17862
18475
|
}
|
|
17863
18476
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
17864
|
-
|
|
18477
|
+
const hooks = toolFiles.filter((f) => f instanceof ToolHooks);
|
|
18478
|
+
const logger = withToolTargetPrefix({
|
|
18479
|
+
logger: this.logger,
|
|
18480
|
+
toolTarget: this.toolTarget
|
|
18481
|
+
});
|
|
18482
|
+
return hooks.map((h) => h.toRulesyncHooks({ logger }));
|
|
17865
18483
|
}
|
|
17866
18484
|
static getToolTargets({ global = false, importOnly = false } = {}) {
|
|
17867
18485
|
if (global) return importOnly ? hooksProcessorToolTargetsGlobalImportable : hooksProcessorToolTargetsGlobal;
|
|
@@ -19504,9 +20122,9 @@ function parseAmpSettingsJsonc(fileContent) {
|
|
|
19504
20122
|
}
|
|
19505
20123
|
function filterMcpServers(mcpServers) {
|
|
19506
20124
|
const filtered = {};
|
|
19507
|
-
if (!isRecord(mcpServers)) return filtered;
|
|
20125
|
+
if (!isRecord$1(mcpServers)) return filtered;
|
|
19508
20126
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
19509
|
-
if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
|
|
20127
|
+
if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
|
|
19510
20128
|
const filteredConfig = {};
|
|
19511
20129
|
for (const [key, value] of Object.entries(config)) {
|
|
19512
20130
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -19621,7 +20239,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
19621
20239
|
success: true,
|
|
19622
20240
|
error: null
|
|
19623
20241
|
};
|
|
19624
|
-
if (!isRecord(mcpServers)) return {
|
|
20242
|
+
if (!isRecord$1(mcpServers)) return {
|
|
19625
20243
|
success: false,
|
|
19626
20244
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
19627
20245
|
};
|
|
@@ -19630,7 +20248,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
19630
20248
|
success: false,
|
|
19631
20249
|
error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
|
|
19632
20250
|
};
|
|
19633
|
-
if (!isRecord(serverConfig)) return {
|
|
20251
|
+
if (!isRecord$1(serverConfig)) return {
|
|
19634
20252
|
success: false,
|
|
19635
20253
|
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
19636
20254
|
};
|
|
@@ -20230,13 +20848,13 @@ function normalizeCodexMcpServerName(name) {
|
|
|
20230
20848
|
function convertFromCodexFormat(codexMcp) {
|
|
20231
20849
|
const result = {};
|
|
20232
20850
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
20233
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
20851
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
20234
20852
|
const converted = {};
|
|
20235
20853
|
for (const [key, value] of Object.entries(config)) {
|
|
20236
20854
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20237
20855
|
if (key === "enabled") {
|
|
20238
20856
|
if (value === false) converted["disabled"] = true;
|
|
20239
|
-
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
|
|
20857
|
+
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
|
|
20240
20858
|
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
20241
20859
|
const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
|
|
20242
20860
|
if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
|
|
@@ -20255,7 +20873,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
20255
20873
|
const result = {};
|
|
20256
20874
|
const originalNames = /* @__PURE__ */ new Map();
|
|
20257
20875
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
20258
|
-
if (!isRecord(config)) continue;
|
|
20876
|
+
if (!isRecord$1(config)) continue;
|
|
20259
20877
|
const { codexName, usedFallback } = normalizeCodexMcpServerName(name);
|
|
20260
20878
|
if (usedFallback) warnWithFallback(void 0, `MCP server "${name}" cannot be represented as a Codex MCP server name (ASCII [a-zA-Z0-9_-] only), so the stable fallback name "${codexName}" was used. Rename the server in .rulesync/mcp.jsonc to choose a readable Codex name.`);
|
|
20261
20879
|
const converted = {};
|
|
@@ -20263,7 +20881,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
20263
20881
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20264
20882
|
if (key === "disabled") {
|
|
20265
20883
|
if (value === true) converted["enabled"] = false;
|
|
20266
|
-
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
|
|
20884
|
+
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
|
|
20267
20885
|
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
20268
20886
|
const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
|
|
20269
20887
|
if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
|
|
@@ -20339,21 +20957,21 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
20339
20957
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
20340
20958
|
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
20341
20959
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
20342
|
-
const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
20960
|
+
const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
20343
20961
|
return [serverName, {
|
|
20344
20962
|
...serverConfig,
|
|
20345
|
-
...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
20346
|
-
...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
|
|
20347
|
-
...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
|
|
20963
|
+
...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
20964
|
+
...isRecord$1(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
|
|
20965
|
+
...isRecord$1(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
|
|
20348
20966
|
}];
|
|
20349
20967
|
})));
|
|
20350
20968
|
const filteredMcpServers = this.removeEmptyEntries(converted);
|
|
20351
20969
|
for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the codex CLI config`);
|
|
20352
|
-
const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
20970
|
+
const existingMcpServers = isRecord$1(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
20353
20971
|
const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
|
|
20354
|
-
const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
20972
|
+
const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
20355
20973
|
const serverRecord = serverConfig;
|
|
20356
|
-
if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
20974
|
+
if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
20357
20975
|
...serverRecord,
|
|
20358
20976
|
tools: existingServer["tools"]
|
|
20359
20977
|
}];
|
|
@@ -21222,6 +21840,27 @@ function resolveGooseType(config, url) {
|
|
|
21222
21840
|
return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
|
|
21223
21841
|
}
|
|
21224
21842
|
/**
|
|
21843
|
+
* The Goose extension types that carry an MCP server. Goose also documents
|
|
21844
|
+
* `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
|
|
21845
|
+
* no canonical MCP counterpart: they name capabilities Goose provides itself
|
|
21846
|
+
* rather than a server rulesync could describe.
|
|
21847
|
+
*/
|
|
21848
|
+
const GOOSE_MCP_EXTENSION_TYPES = /* @__PURE__ */ new Set([
|
|
21849
|
+
"stdio",
|
|
21850
|
+
"streamable_http",
|
|
21851
|
+
"sse"
|
|
21852
|
+
]);
|
|
21853
|
+
/**
|
|
21854
|
+
* Resolves the Goose extension type of an existing `extensions:` entry the way
|
|
21855
|
+
* Goose itself reads it: the declared `type`, or the shape of the entry when
|
|
21856
|
+
* the key is absent.
|
|
21857
|
+
*/
|
|
21858
|
+
function existingExtensionType(ext) {
|
|
21859
|
+
if (typeof ext.type === "string") return ext.type;
|
|
21860
|
+
if (typeof ext.cmd === "string") return "stdio";
|
|
21861
|
+
if (typeof ext.uri === "string") return "streamable_http";
|
|
21862
|
+
}
|
|
21863
|
+
/**
|
|
21225
21864
|
* Resolves the canonical timeout for a server (`timeout` or `networkTimeout`).
|
|
21226
21865
|
*/
|
|
21227
21866
|
function resolveGooseTimeout(config) {
|
|
@@ -21247,15 +21886,20 @@ function applyGooseStdioFields(ext, config) {
|
|
|
21247
21886
|
/**
|
|
21248
21887
|
* Converts a single rulesync canonical MCP server into a Goose `extensions:` entry.
|
|
21249
21888
|
*/
|
|
21250
|
-
function convertServerToGooseExtension(name, config) {
|
|
21889
|
+
function convertServerToGooseExtension(name, config, logger) {
|
|
21251
21890
|
const url = resolveGooseUrl(config);
|
|
21252
21891
|
const gooseType = resolveGooseType(config, url);
|
|
21253
21892
|
const ext = {
|
|
21254
21893
|
name,
|
|
21255
21894
|
type: gooseType
|
|
21256
21895
|
};
|
|
21257
|
-
if (gooseType === "stdio")
|
|
21258
|
-
|
|
21896
|
+
if (gooseType === "stdio") {
|
|
21897
|
+
applyGooseStdioFields(ext, config);
|
|
21898
|
+
if (typeof ext.cmd !== "string" || ext.cmd === "") {
|
|
21899
|
+
warnWithFallback(logger, `Goose extension "${name}" has no command to run; skipping it rather than writing a stdio extension Goose cannot start to ~/.config/goose/config.yaml.`);
|
|
21900
|
+
return;
|
|
21901
|
+
}
|
|
21902
|
+
} else if (gooseType === "sse" || gooseType === "streamable_http") {
|
|
21259
21903
|
if (url !== void 0) ext.uri = url;
|
|
21260
21904
|
if (isPlainObject$1(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
|
|
21261
21905
|
}
|
|
@@ -21270,13 +21914,41 @@ function convertServerToGooseExtension(name, config) {
|
|
|
21270
21914
|
* Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
|
|
21271
21915
|
* | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
|
|
21272
21916
|
* remote, plus `enabled` and `timeout`.
|
|
21273
|
-
|
|
21274
|
-
|
|
21275
|
-
|
|
21917
|
+
*
|
|
21918
|
+
* `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
|
|
21919
|
+
* holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
|
|
21920
|
+
* (`developer`, `memory`, ...), which have no canonical MCP representation.
|
|
21921
|
+
* Those entries are carried over from `existingExtensions` untouched — removing
|
|
21922
|
+
* `developer` alone costs the agent its shell and text-editor tools. Only an
|
|
21923
|
+
* entry rulesync can positively identify as an MCP server is rulesync's to
|
|
21924
|
+
* replace, so a server deleted from `.rulesync/.mcp.json` is retracted (with a
|
|
21925
|
+
* warning naming it) while an entry of an unrecognized shape or a future
|
|
21926
|
+
* extension type is left alone rather than assumed to be ours.
|
|
21927
|
+
*/
|
|
21928
|
+
function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
|
|
21929
|
+
const generated = {};
|
|
21276
21930
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21277
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
21278
|
-
|
|
21931
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21932
|
+
const ext = convertServerToGooseExtension(name, config, logger);
|
|
21933
|
+
if (ext !== void 0) generated[name] = ext;
|
|
21279
21934
|
}
|
|
21935
|
+
const extensions = {};
|
|
21936
|
+
const retracted = [];
|
|
21937
|
+
for (const [name, ext] of Object.entries(existingExtensions)) {
|
|
21938
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
21939
|
+
const type = isRecord$1(ext) ? existingExtensionType(ext) : void 0;
|
|
21940
|
+
if (type !== void 0 && GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21941
|
+
if (!Object.hasOwn(generated, name)) retracted.push(name);
|
|
21942
|
+
continue;
|
|
21943
|
+
}
|
|
21944
|
+
if (!Object.hasOwn(generated, name)) {
|
|
21945
|
+
extensions[name] = ext;
|
|
21946
|
+
continue;
|
|
21947
|
+
}
|
|
21948
|
+
if (generated[name]?.type !== type) warnWithFallback(logger, `Goose extension "${name}" already exists in config.yaml as a non-MCP extension; the MCP server of the same name replaces it.`);
|
|
21949
|
+
}
|
|
21950
|
+
if (retracted.length > 0) warnWithFallback(logger, `Removing MCP extension(s) ${retracted.map((name) => `"${name}"`).join(", ")} from ~/.config/goose/config.yaml: they are not in the generated rulesync MCP config. Import them first if they were added with \`goose configure\`.`);
|
|
21951
|
+
Object.assign(extensions, generated);
|
|
21280
21952
|
return extensions;
|
|
21281
21953
|
}
|
|
21282
21954
|
/**
|
|
@@ -21287,16 +21959,27 @@ function convertToGooseFormat(mcpServers) {
|
|
|
21287
21959
|
* so both `url` and the Claude-specific `httpUrl` alias come back as `url`; and
|
|
21288
21960
|
* the `streamable_http` type maps back to canonical `http`. These are the
|
|
21289
21961
|
* canonical/preferred forms, so re-generating produces an equivalent config.
|
|
21962
|
+
*
|
|
21963
|
+
* Non-MCP extension types (`builtin`, `platform`, `frontend`, `inline_python`)
|
|
21964
|
+
* are skipped: they describe capabilities Goose provides itself, and importing
|
|
21965
|
+
* one would strip the type that makes it work — a `builtin` entry came back as
|
|
21966
|
+
* a `stdio` extension with no `cmd` that Goose cannot start. They stay in `config.yaml`,
|
|
21967
|
+
* which generation preserves.
|
|
21290
21968
|
*/
|
|
21291
21969
|
function convertFromGooseFormat(extensions) {
|
|
21292
21970
|
const result = {};
|
|
21971
|
+
const skipped = [];
|
|
21293
21972
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
21294
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
21973
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
|
|
21974
|
+
const type = existingExtensionType(ext);
|
|
21975
|
+
if (type === void 0 || !GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21976
|
+
skipped.push(name);
|
|
21977
|
+
continue;
|
|
21978
|
+
}
|
|
21295
21979
|
const server = {};
|
|
21296
|
-
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
21297
21980
|
if (type === "sse") server.type = "sse";
|
|
21298
21981
|
else if (type === "streamable_http") server.type = "http";
|
|
21299
|
-
else
|
|
21982
|
+
else server.type = "stdio";
|
|
21300
21983
|
if (typeof ext.cmd === "string") server.command = ext.cmd;
|
|
21301
21984
|
if (isStringArray$1(ext.args)) server.args = ext.args;
|
|
21302
21985
|
if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
|
|
@@ -21306,6 +21989,7 @@ function convertFromGooseFormat(extensions) {
|
|
|
21306
21989
|
if (typeof ext.timeout === "number") server.timeout = ext.timeout;
|
|
21307
21990
|
result[name] = server;
|
|
21308
21991
|
}
|
|
21992
|
+
if (skipped.length > 0) warnWithFallback(void 0, `Skipping ${skipped.length} non-MCP Goose extension(s) (${skipped.map((name) => `"${name}"`).join(", ")}): they describe capabilities Goose provides itself and have no rulesync representation.`);
|
|
21309
21993
|
return result;
|
|
21310
21994
|
}
|
|
21311
21995
|
/**
|
|
@@ -21340,7 +22024,7 @@ function buildGoosePluginStdioServer(config) {
|
|
|
21340
22024
|
function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
21341
22025
|
const result = {};
|
|
21342
22026
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21343
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22027
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21344
22028
|
const gooseType = resolveGooseType(config, resolveGooseUrl(config));
|
|
21345
22029
|
if (gooseType !== "stdio") {
|
|
21346
22030
|
warnWithFallback(logger, `Goose open-plugin MCP manifest (${GOOSE_PLUGIN_MCP_RELATIVE_PATH}) is stdio-only; skipping "${name}" (${gooseType}). Sync it with --global to ~/.config/goose/config.yaml instead.`);
|
|
@@ -21381,7 +22065,7 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
21381
22065
|
parsePluginManifest(fileContent) {
|
|
21382
22066
|
try {
|
|
21383
22067
|
const parsed = JSON.parse(fileContent);
|
|
21384
|
-
return isRecord(parsed) ? parsed : {};
|
|
22068
|
+
return isRecord$1(parsed) ? parsed : {};
|
|
21385
22069
|
} catch (error) {
|
|
21386
22070
|
throw new Error(`Failed to parse Goose MCP manifest at ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
21387
22071
|
}
|
|
@@ -21427,25 +22111,35 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
21427
22111
|
global
|
|
21428
22112
|
});
|
|
21429
22113
|
}
|
|
21430
|
-
const
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
};
|
|
22114
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22115
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
22116
|
+
const config = parseGooseConfig(existingContent, paths.relativeDirPath, paths.relativeFilePath);
|
|
22117
|
+
const existingExtensions = isRecord$1(config.extensions) ? config.extensions : {};
|
|
21434
22118
|
return new GooseMcp({
|
|
21435
22119
|
outputRoot,
|
|
21436
22120
|
relativeDirPath: paths.relativeDirPath,
|
|
21437
22121
|
relativeFilePath: paths.relativeFilePath,
|
|
21438
|
-
fileContent: (
|
|
22122
|
+
fileContent: applySharedConfigPatch({
|
|
22123
|
+
fileKey: sharedConfigFileKey(paths),
|
|
22124
|
+
feature: "mcp",
|
|
22125
|
+
existingContent,
|
|
22126
|
+
patch: { extensions: convertToGooseFormat({
|
|
22127
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
22128
|
+
existingExtensions,
|
|
22129
|
+
logger
|
|
22130
|
+
}) },
|
|
22131
|
+
filePath
|
|
22132
|
+
}),
|
|
21439
22133
|
validate,
|
|
21440
22134
|
global
|
|
21441
22135
|
});
|
|
21442
22136
|
}
|
|
21443
22137
|
toRulesyncMcp() {
|
|
21444
22138
|
if (!this.global) {
|
|
21445
|
-
const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
22139
|
+
const mcpServers = isRecord$1(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
21446
22140
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
21447
22141
|
}
|
|
21448
|
-
const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
|
|
22142
|
+
const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
|
|
21449
22143
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
21450
22144
|
}
|
|
21451
22145
|
validate() {
|
|
@@ -21481,7 +22175,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
21481
22175
|
const result = {};
|
|
21482
22176
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21483
22177
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
21484
|
-
if (!isRecord(config)) continue;
|
|
22178
|
+
if (!isRecord$1(config)) continue;
|
|
21485
22179
|
const converted = {};
|
|
21486
22180
|
for (const [key, value] of Object.entries(config)) {
|
|
21487
22181
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -21496,7 +22190,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
21496
22190
|
function convertFromGrokFormat(grokMcp) {
|
|
21497
22191
|
const result = {};
|
|
21498
22192
|
for (const [name, config] of Object.entries(grokMcp)) {
|
|
21499
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22193
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21500
22194
|
const converted = {};
|
|
21501
22195
|
for (const [key, value] of Object.entries(config)) {
|
|
21502
22196
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -21634,7 +22328,7 @@ function resolveHermesTimeout(config) {
|
|
|
21634
22328
|
* import alike. See the Hermes mcp-config-reference.
|
|
21635
22329
|
*/
|
|
21636
22330
|
function copyHermesOauth(source) {
|
|
21637
|
-
if (!isRecord(source)) return;
|
|
22331
|
+
if (!isRecord$1(source)) return;
|
|
21638
22332
|
const oauth = {};
|
|
21639
22333
|
for (const key of [
|
|
21640
22334
|
"redirect_uri",
|
|
@@ -21768,13 +22462,13 @@ function convertServerToHermes(config) {
|
|
|
21768
22462
|
function convertToHermesFormat(mcpServers) {
|
|
21769
22463
|
const result = {};
|
|
21770
22464
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21771
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22465
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21772
22466
|
result[name] = convertServerToHermes(config);
|
|
21773
22467
|
}
|
|
21774
22468
|
return result;
|
|
21775
22469
|
}
|
|
21776
22470
|
function mergeHermesMcpServers(config, mcpServers) {
|
|
21777
|
-
const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
|
|
22471
|
+
const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
|
|
21778
22472
|
return {
|
|
21779
22473
|
...config,
|
|
21780
22474
|
mcp_servers: {
|
|
@@ -21793,7 +22487,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
21793
22487
|
const result = {};
|
|
21794
22488
|
const hermesOverrides = {};
|
|
21795
22489
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21796
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22490
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21797
22491
|
const server = {};
|
|
21798
22492
|
if (typeof config.command === "string") server.command = config.command;
|
|
21799
22493
|
if (isStringArray$1(config.args)) server.args = config.args;
|
|
@@ -21802,7 +22496,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
21802
22496
|
if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
21803
22497
|
if (config.enabled === false) server.disabled = true;
|
|
21804
22498
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
21805
|
-
if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
22499
|
+
if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
21806
22500
|
result[name] = server;
|
|
21807
22501
|
const hermesServer = { ...server };
|
|
21808
22502
|
if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
|
|
@@ -21841,7 +22535,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
21841
22535
|
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
21842
22536
|
format: "yaml",
|
|
21843
22537
|
fileContent
|
|
21844
|
-
}), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
22538
|
+
}), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
21845
22539
|
this.config = merged;
|
|
21846
22540
|
super.setFileContent(applySharedConfigPatch({
|
|
21847
22541
|
fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
|
|
@@ -21905,7 +22599,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
21905
22599
|
});
|
|
21906
22600
|
}
|
|
21907
22601
|
toRulesyncMcp() {
|
|
21908
|
-
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
22602
|
+
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
21909
22603
|
return this.toRulesyncMcpDefault({
|
|
21910
22604
|
outputRoot: getHermesagentRulesyncOutputRoot({
|
|
21911
22605
|
nativeOutputRoot: this.outputRoot,
|
|
@@ -22217,7 +22911,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
|
|
|
22217
22911
|
*/
|
|
22218
22912
|
function readExistingKiloMcpEntries(fileContent) {
|
|
22219
22913
|
const mcp = (0, jsonc_parser.parse)(fileContent || "{}")?.mcp;
|
|
22220
|
-
if (!isRecord(mcp)) return {};
|
|
22914
|
+
if (!isRecord$1(mcp)) return {};
|
|
22221
22915
|
const entries = {};
|
|
22222
22916
|
for (const [serverName, entry] of Object.entries(mcp)) {
|
|
22223
22917
|
const result = KiloMcpServerSchema.safeParse(entry);
|
|
@@ -22521,7 +23215,7 @@ async function readKimiCodeConfig({ outputRoot }) {
|
|
|
22521
23215
|
return {
|
|
22522
23216
|
parsed: true,
|
|
22523
23217
|
content,
|
|
22524
|
-
mcp: isRecord(mcp) ? mcp : {}
|
|
23218
|
+
mcp: isRecord$1(mcp) ? mcp : {}
|
|
22525
23219
|
};
|
|
22526
23220
|
} catch {
|
|
22527
23221
|
return {
|
|
@@ -22675,7 +23369,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
22675
23369
|
static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
|
|
22676
23370
|
if (!global) return [];
|
|
22677
23371
|
const block = rulesyncMcp.getJson()["kimi-code"];
|
|
22678
|
-
if (!isRecord(block)) return [];
|
|
23372
|
+
if (!isRecord$1(block)) return [];
|
|
22679
23373
|
const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
|
|
22680
23374
|
const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
|
|
22681
23375
|
if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0) return [];
|
|
@@ -22728,6 +23422,68 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
22728
23422
|
};
|
|
22729
23423
|
//#endregion
|
|
22730
23424
|
//#region src/features/mcp/kiro-mcp.ts
|
|
23425
|
+
/**
|
|
23426
|
+
* Union of two optional string lists, preserving order and dropping duplicates.
|
|
23427
|
+
* Returns `undefined` only when neither side was authored at all, so the caller
|
|
23428
|
+
* omits the key entirely rather than writing an empty array — but an explicitly
|
|
23429
|
+
* authored `[]` is kept, which keeps import → generate idempotent.
|
|
23430
|
+
*/
|
|
23431
|
+
function mergeToolLists(...lists) {
|
|
23432
|
+
if (lists.every((list) => list === void 0)) return void 0;
|
|
23433
|
+
const merged = [];
|
|
23434
|
+
for (const list of lists) for (const tool of list ?? []) if (!merged.includes(tool)) merged.push(tool);
|
|
23435
|
+
return merged;
|
|
23436
|
+
}
|
|
23437
|
+
/**
|
|
23438
|
+
* Translate rulesync's Kiro-only authoring keys onto the field names Kiro
|
|
23439
|
+
* actually reads in `mcp.json`.
|
|
23440
|
+
*
|
|
23441
|
+
* - `kiroAutoApprove` → `autoApprove` (tools run without a confirmation prompt)
|
|
23442
|
+
* - `kiroAutoBlock` → `disabledTools` (tools hidden from the agent)
|
|
23443
|
+
*
|
|
23444
|
+
* `disabledTools` is the only block list Kiro reads, and it is also a canonical
|
|
23445
|
+
* rulesync field, so `kiroAutoBlock` is a redundant spelling of it. Prefer the
|
|
23446
|
+
* canonical field; see the note on `kiroAutoBlock` in `src/types/mcp.ts`.
|
|
23447
|
+
*
|
|
23448
|
+
* Both native names are documented per-server fields, so a config that already
|
|
23449
|
+
* spells them natively keeps working: the two lists are merged rather than
|
|
23450
|
+
* one overwriting the other.
|
|
23451
|
+
* @see https://kiro.dev/docs/mcp/configuration/
|
|
23452
|
+
*/
|
|
23453
|
+
function toKiroMcpServers(servers) {
|
|
23454
|
+
return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
|
|
23455
|
+
const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
|
|
23456
|
+
const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
|
|
23457
|
+
const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
|
|
23458
|
+
return [name, {
|
|
23459
|
+
...rest,
|
|
23460
|
+
...autoApprove !== void 0 && { autoApprove },
|
|
23461
|
+
...disabled !== void 0 && { disabledTools: disabled }
|
|
23462
|
+
}];
|
|
23463
|
+
}));
|
|
23464
|
+
}
|
|
23465
|
+
/**
|
|
23466
|
+
* Import direction of {@link toKiroMcpServers}: Kiro's `autoApprove` becomes the
|
|
23467
|
+
* rulesync-only `kiroAutoApprove` so a regenerate reproduces it. `disabledTools`
|
|
23468
|
+
* is left alone — it is already a canonical rulesync key with the same meaning,
|
|
23469
|
+
* so `kiroAutoBlock` deliberately has no import counterpart.
|
|
23470
|
+
*
|
|
23471
|
+
* Only a genuine string array is renamed. `kiroAutoApprove` is typed as one, so
|
|
23472
|
+
* moving a hand-written `"autoApprove": "all"` there would produce a
|
|
23473
|
+
* `.rulesync/mcp.jsonc` the next generate refuses to parse; such a value stays
|
|
23474
|
+
* under its original key and passes through untouched instead.
|
|
23475
|
+
*/
|
|
23476
|
+
function fromKiroMcpServers(servers) {
|
|
23477
|
+
return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
|
|
23478
|
+
if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
|
|
23479
|
+
const { autoApprove, ...rest } = server;
|
|
23480
|
+
if (!isStringArray$1(autoApprove)) return [name, server];
|
|
23481
|
+
return [name, {
|
|
23482
|
+
...rest,
|
|
23483
|
+
kiroAutoApprove: autoApprove
|
|
23484
|
+
}];
|
|
23485
|
+
}));
|
|
23486
|
+
}
|
|
22731
23487
|
var KiroMcp = class KiroMcp extends ToolMcp {
|
|
22732
23488
|
json;
|
|
22733
23489
|
constructor(params) {
|
|
@@ -22756,7 +23512,7 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
22756
23512
|
}
|
|
22757
23513
|
static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
|
|
22758
23514
|
const paths = this.getSettablePaths();
|
|
22759
|
-
const fileContent = JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2);
|
|
23515
|
+
const fileContent = JSON.stringify({ mcpServers: toKiroMcpServers(rulesyncMcp.getMcpServers()) }, null, 2);
|
|
22760
23516
|
return new KiroMcp({
|
|
22761
23517
|
outputRoot,
|
|
22762
23518
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -22766,7 +23522,9 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
22766
23522
|
});
|
|
22767
23523
|
}
|
|
22768
23524
|
toRulesyncMcp() {
|
|
22769
|
-
|
|
23525
|
+
const mcpServers = this.json.mcpServers;
|
|
23526
|
+
const translated = isMcpServers(mcpServers) ? fromKiroMcpServers(mcpServers) : {};
|
|
23527
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: translated }, null, 2) });
|
|
22770
23528
|
}
|
|
22771
23529
|
validate() {
|
|
22772
23530
|
return {
|
|
@@ -23652,7 +24410,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
|
|
|
23652
24410
|
});
|
|
23653
24411
|
}
|
|
23654
24412
|
function disabledNamesOf(config) {
|
|
23655
|
-
const mcpBlock = config && isRecord(config.mcp) ? config.mcp : {};
|
|
24413
|
+
const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
|
|
23656
24414
|
return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
23657
24415
|
}
|
|
23658
24416
|
/**
|
|
@@ -23771,7 +24529,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
23771
24529
|
const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
|
|
23772
24530
|
const disabledNames = managedNames.filter((name) => {
|
|
23773
24531
|
const server = servers[name];
|
|
23774
|
-
return isRecord(server) && server.disabled === true;
|
|
24532
|
+
return isRecord$1(server) && server.disabled === true;
|
|
23775
24533
|
});
|
|
23776
24534
|
const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)) ?? "";
|
|
23777
24535
|
let existingParsed;
|
|
@@ -23785,7 +24543,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
23785
24543
|
logger?.warn(`Skipping the Rovo Dev mcp.disabledMcpServers update: ${formatError(error)}`);
|
|
23786
24544
|
return [];
|
|
23787
24545
|
}
|
|
23788
|
-
const existingMcp = isRecord(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
|
|
24546
|
+
const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
|
|
23789
24547
|
const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
|
|
23790
24548
|
const managedNameSet = new Set(managedNames);
|
|
23791
24549
|
const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
|
|
@@ -23977,7 +24735,7 @@ function deriveTransportAllowlist(servers) {
|
|
|
23977
24735
|
http: false
|
|
23978
24736
|
};
|
|
23979
24737
|
for (const server of Object.values(servers)) {
|
|
23980
|
-
if (!isRecord(server)) continue;
|
|
24738
|
+
if (!isRecord$1(server)) continue;
|
|
23981
24739
|
const transport = transportOf(server);
|
|
23982
24740
|
if (transport) allowlist[transport] = true;
|
|
23983
24741
|
}
|
|
@@ -27612,13 +28370,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
27612
28370
|
WebFetch: "webfetch",
|
|
27613
28371
|
Mcp: "mcp"
|
|
27614
28372
|
};
|
|
27615
|
-
const MCP_CANONICAL_PREFIX$
|
|
28373
|
+
const MCP_CANONICAL_PREFIX$2 = "mcp__";
|
|
27616
28374
|
/**
|
|
27617
28375
|
* Returns true if the canonical category is the per-tool MCP form
|
|
27618
28376
|
* `mcp__<server>__<tool>`.
|
|
27619
28377
|
*/
|
|
27620
28378
|
function isMcpScopedCategory(canonical) {
|
|
27621
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
28379
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$2) && canonical.length > 5;
|
|
27622
28380
|
}
|
|
27623
28381
|
function toCursorType(canonical) {
|
|
27624
28382
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -27652,7 +28410,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
27652
28410
|
if (match) {
|
|
27653
28411
|
const server = match[1] ?? "*";
|
|
27654
28412
|
const tool = match[2] ?? "*";
|
|
27655
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
28413
|
+
return `${MCP_CANONICAL_PREFIX$2}${server}__${tool}`;
|
|
27656
28414
|
}
|
|
27657
28415
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
27658
28416
|
}
|
|
@@ -27881,7 +28639,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
27881
28639
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
27882
28640
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
27883
28641
|
if (!permission[canonical]) permission[canonical] = {};
|
|
27884
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
28642
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$2) ? "*" : pattern;
|
|
27885
28643
|
permission[canonical][canonicalPattern] = action;
|
|
27886
28644
|
}
|
|
27887
28645
|
};
|
|
@@ -28016,14 +28774,14 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
28016
28774
|
let settings;
|
|
28017
28775
|
try {
|
|
28018
28776
|
const parsed = JSON.parse(existingContent);
|
|
28019
|
-
settings = isRecord(parsed) ? parsed : {};
|
|
28777
|
+
settings = isRecord$1(parsed) ? parsed : {};
|
|
28020
28778
|
} catch (error) {
|
|
28021
28779
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
28022
28780
|
}
|
|
28023
28781
|
const config = rulesyncPermissions.getJson();
|
|
28024
28782
|
const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
|
|
28025
28783
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
28026
|
-
const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
28784
|
+
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
28027
28785
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
28028
28786
|
const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
28029
28787
|
const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
|
|
@@ -28053,11 +28811,11 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
28053
28811
|
let settings;
|
|
28054
28812
|
try {
|
|
28055
28813
|
const parsed = JSON.parse(this.getFileContent());
|
|
28056
|
-
settings = isRecord(parsed) ? parsed : {};
|
|
28814
|
+
settings = isRecord$1(parsed) ? parsed : {};
|
|
28057
28815
|
} catch (error) {
|
|
28058
28816
|
throw new Error(`Failed to parse Devin permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28059
28817
|
}
|
|
28060
|
-
const permissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
28818
|
+
const permissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
28061
28819
|
const config = convertDevinToRulesyncPermissions({
|
|
28062
28820
|
allow: Array.isArray(permissions.allow) ? permissions.allow : [],
|
|
28063
28821
|
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
@@ -28144,7 +28902,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
|
|
|
28144
28902
|
"interactionMode",
|
|
28145
28903
|
"extraKnownMarketplaces",
|
|
28146
28904
|
"enabledPlugins",
|
|
28147
|
-
"hooksDisabled"
|
|
28905
|
+
"hooksDisabled",
|
|
28906
|
+
"disabledSkills"
|
|
28148
28907
|
];
|
|
28149
28908
|
/**
|
|
28150
28909
|
* Permissions adapter for Factory Droid.
|
|
@@ -28405,7 +29164,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
28405
29164
|
} catch (error) {
|
|
28406
29165
|
throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
28407
29166
|
}
|
|
28408
|
-
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
29167
|
+
const config = isRecord$1(parsed) ? { ...parsed } : {};
|
|
28409
29168
|
const userPermission = convertRulesyncToGoosePermissionConfig({
|
|
28410
29169
|
config: rulesyncPermissions.getJson(),
|
|
28411
29170
|
logger
|
|
@@ -28428,8 +29187,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
28428
29187
|
} catch (error) {
|
|
28429
29188
|
throw new Error(`Failed to parse Goose permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28430
29189
|
}
|
|
28431
|
-
const config = isRecord(parsed) ? parsed : {};
|
|
28432
|
-
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
29190
|
+
const config = isRecord$1(parsed) ? parsed : {};
|
|
29191
|
+
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
28433
29192
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
28434
29193
|
}
|
|
28435
29194
|
validate() {
|
|
@@ -28503,7 +29262,7 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
28503
29262
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
28504
29263
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
28505
29264
|
const CATCH_ALL_PATTERN$2 = "*";
|
|
28506
|
-
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
29265
|
+
const MCP_CANONICAL_PREFIX$1 = "mcp__";
|
|
28507
29266
|
const CATEGORY_TO_GROK_TOOL = {
|
|
28508
29267
|
bash: "Bash",
|
|
28509
29268
|
read: "Read",
|
|
@@ -28531,7 +29290,7 @@ const GROK_MCP_TOOL = "MCPTool";
|
|
|
28531
29290
|
* concrete pattern emits `Tool(pattern)`.
|
|
28532
29291
|
*/
|
|
28533
29292
|
function buildGrokEntry(category, pattern) {
|
|
28534
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
29293
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$1)) {
|
|
28535
29294
|
const remainder = category.slice(5);
|
|
28536
29295
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
28537
29296
|
}
|
|
@@ -28558,7 +29317,7 @@ function parseGrokEntry(entry) {
|
|
|
28558
29317
|
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
28559
29318
|
}
|
|
28560
29319
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
28561
|
-
category: `${MCP_CANONICAL_PREFIX}${inner}`,
|
|
29320
|
+
category: `${MCP_CANONICAL_PREFIX$1}${inner}`,
|
|
28562
29321
|
pattern: CATCH_ALL_PATTERN$2
|
|
28563
29322
|
} : {
|
|
28564
29323
|
category: "mcp",
|
|
@@ -28656,7 +29415,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
28656
29415
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
28657
29416
|
}
|
|
28658
29417
|
const config = rulesyncPermissions.getJson();
|
|
28659
|
-
const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
29418
|
+
const existingPermission = isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
28660
29419
|
const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
|
|
28661
29420
|
const permission = {
|
|
28662
29421
|
...existingPermission,
|
|
@@ -28665,7 +29424,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
28665
29424
|
ask: buckets.ask
|
|
28666
29425
|
};
|
|
28667
29426
|
const uiPatch = global ? { [GROKCLI_UI_KEY]: {
|
|
28668
|
-
...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
|
|
29427
|
+
...isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
|
|
28669
29428
|
[GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
|
|
28670
29429
|
} } : {};
|
|
28671
29430
|
return new GrokcliPermissions({
|
|
@@ -28694,7 +29453,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
28694
29453
|
} catch (error) {
|
|
28695
29454
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28696
29455
|
}
|
|
28697
|
-
const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
29456
|
+
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
28698
29457
|
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
|
|
28699
29458
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
28700
29459
|
}
|
|
@@ -28791,7 +29550,7 @@ function parseGrokPermissionArrays(permission) {
|
|
|
28791
29550
|
* `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
|
|
28792
29551
|
*/
|
|
28793
29552
|
function legacyModeAction(parsed) {
|
|
28794
|
-
return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
29553
|
+
return (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
28795
29554
|
}
|
|
28796
29555
|
/**
|
|
28797
29556
|
* Collapse a rulesync permissions config into Grok's single coarse mode.
|
|
@@ -28847,12 +29606,12 @@ function withoutKey(record, key) {
|
|
|
28847
29606
|
return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
|
|
28848
29607
|
}
|
|
28849
29608
|
function buildHermesOverride(config, provenance) {
|
|
28850
|
-
const base = isRecord(provenance.hermes) ? { ...provenance.hermes } : {};
|
|
28851
|
-
const approvalsOverride = withoutKey(isRecord(config.approvals) ? config.approvals : {}, "deny");
|
|
29609
|
+
const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
|
|
29610
|
+
const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
|
|
28852
29611
|
if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
|
|
28853
29612
|
else delete base.approvals;
|
|
28854
|
-
const security = isRecord(config.security) ? { ...config.security } : {};
|
|
28855
|
-
const blocklist = isRecord(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
|
|
29613
|
+
const security = isRecord$1(config.security) ? { ...config.security } : {};
|
|
29614
|
+
const blocklist = isRecord$1(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
|
|
28856
29615
|
if (blocklist?.enabled === true) {
|
|
28857
29616
|
delete blocklist.domains;
|
|
28858
29617
|
delete blocklist.enabled;
|
|
@@ -28861,7 +29620,7 @@ function buildHermesOverride(config, provenance) {
|
|
|
28861
29620
|
else delete security.website_blocklist;
|
|
28862
29621
|
if (Object.keys(security).length > 0) base.security = security;
|
|
28863
29622
|
else delete base.security;
|
|
28864
|
-
for (const key of ["skills", "memory"]) if (isRecord(config[key])) base[key] = config[key];
|
|
29623
|
+
for (const key of ["skills", "memory"]) if (isRecord$1(config[key])) base[key] = config[key];
|
|
28865
29624
|
else delete base[key];
|
|
28866
29625
|
return base;
|
|
28867
29626
|
}
|
|
@@ -28933,7 +29692,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
28933
29692
|
format: "yaml",
|
|
28934
29693
|
fileContent: this.getFileContent()
|
|
28935
29694
|
});
|
|
28936
|
-
const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
|
|
29695
|
+
const permissionsRoot = isRecord$1(config.permissions) ? config.permissions : {};
|
|
28937
29696
|
const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(permissionsRoot.rulesync);
|
|
28938
29697
|
const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
|
|
28939
29698
|
const permission = clonePermissionBlock(provenance.permission);
|
|
@@ -28941,14 +29700,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
28941
29700
|
permission,
|
|
28942
29701
|
commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
|
|
28943
29702
|
});
|
|
28944
|
-
const approvals = isRecord(config.approvals) ? config.approvals : {};
|
|
29703
|
+
const approvals = isRecord$1(config.approvals) ? config.approvals : {};
|
|
28945
29704
|
reconcileNativeDenies({
|
|
28946
29705
|
permission,
|
|
28947
29706
|
category: "bash",
|
|
28948
29707
|
patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
|
|
28949
29708
|
});
|
|
28950
|
-
const security = isRecord(config.security) ? config.security : {};
|
|
28951
|
-
const websiteBlocklist = isRecord(security.website_blocklist) ? security.website_blocklist : {};
|
|
29709
|
+
const security = isRecord$1(config.security) ? config.security : {};
|
|
29710
|
+
const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
|
|
28952
29711
|
reconcileNativeDenies({
|
|
28953
29712
|
permission,
|
|
28954
29713
|
category: "webfetch",
|
|
@@ -29623,8 +30382,8 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
|
|
|
29623
30382
|
existing = void 0;
|
|
29624
30383
|
}
|
|
29625
30384
|
const merged = {
|
|
29626
|
-
...isRecord(existing) ? existing : {},
|
|
29627
|
-
...isRecord(patch.tools) ? patch.tools : {}
|
|
30385
|
+
...isRecord$1(existing) ? existing : {},
|
|
30386
|
+
...isRecord$1(patch.tools) ? patch.tools : {}
|
|
29628
30387
|
};
|
|
29629
30388
|
if (Object.keys(merged).length === 0) return;
|
|
29630
30389
|
warnAboutMistypedToolLists(merged);
|
|
@@ -29648,7 +30407,7 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
|
|
|
29648
30407
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#tools
|
|
29649
30408
|
*/
|
|
29650
30409
|
function buildKimiCodeToolsSection(tools) {
|
|
29651
|
-
if (!isRecord(tools)) return;
|
|
30410
|
+
if (!isRecord$1(tools)) return;
|
|
29652
30411
|
const section = Object.fromEntries(Object.entries(tools).filter(([key, value]) => key === "enabled" || key === "disabled" ? isStringList(value) : true));
|
|
29653
30412
|
return Object.keys(section).length > 0 ? section : void 0;
|
|
29654
30413
|
}
|
|
@@ -29747,7 +30506,7 @@ function preserveKimiCodeRules(rules) {
|
|
|
29747
30506
|
nativeRules
|
|
29748
30507
|
};
|
|
29749
30508
|
for (const raw of rules) {
|
|
29750
|
-
if (!isRecord(raw)) continue;
|
|
30509
|
+
if (!isRecord$1(raw)) continue;
|
|
29751
30510
|
const decision = raw.decision;
|
|
29752
30511
|
const pattern = raw.pattern;
|
|
29753
30512
|
if (decision !== "allow" && decision !== "ask" && decision !== "deny" || typeof pattern !== "string") continue;
|
|
@@ -29862,7 +30621,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
|
|
|
29862
30621
|
format: "toml",
|
|
29863
30622
|
fileContent: this.getFileContent()
|
|
29864
30623
|
});
|
|
29865
|
-
const { permission, nativeRules } = preserveKimiCodeRules((isRecord(config.permission) ? config.permission : {}).rules);
|
|
30624
|
+
const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
|
|
29866
30625
|
const defaultPermissionMode = config.default_permission_mode;
|
|
29867
30626
|
const tools = buildKimiCodeToolsSection(config.tools);
|
|
29868
30627
|
const toolOverride = {
|
|
@@ -31139,7 +31898,7 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
31139
31898
|
} catch (error) {
|
|
31140
31899
|
throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
31141
31900
|
}
|
|
31142
|
-
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
31901
|
+
const config = isRecord$1(parsed) ? { ...parsed } : {};
|
|
31143
31902
|
const rulesyncConfig = rulesyncPermissions.getJson();
|
|
31144
31903
|
const toolPermissions = convertRulesyncToRovodevToolPermissions({
|
|
31145
31904
|
config: rulesyncConfig,
|
|
@@ -31176,8 +31935,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
31176
31935
|
} catch (error) {
|
|
31177
31936
|
throw new Error(`Failed to parse Rovodev permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
31178
31937
|
}
|
|
31179
|
-
const config = isRecord(parsed) ? parsed : {};
|
|
31180
|
-
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
|
|
31938
|
+
const config = isRecord$1(parsed) ? parsed : {};
|
|
31939
|
+
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
|
|
31181
31940
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
31182
31941
|
}
|
|
31183
31942
|
validate() {
|
|
@@ -31204,14 +31963,14 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
31204
31963
|
* keys it does not are kept as-is.
|
|
31205
31964
|
*/
|
|
31206
31965
|
function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
|
|
31207
|
-
const existingToolPermissions = isRecord(existing) ? { ...existing } : {};
|
|
31966
|
+
const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
|
|
31208
31967
|
if (Object.keys(generated).length === 0 && sourceStatesRules) {
|
|
31209
|
-
if (!isRecord(existing)) return;
|
|
31968
|
+
if (!isRecord$1(existing)) return;
|
|
31210
31969
|
const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
|
|
31211
31970
|
logger?.warn(`Rovo Dev permissions: the rulesync source produced no rule Rovo Dev can express, so the toolPermissions block in ${filePath} keeps its current levels` + (strippedKeys.length > 0 ? `, minus the grants ${strippedKeys.map((key) => `"${key}"`).join(", ")}.` : `.`));
|
|
31212
31971
|
return existingToolPermissions;
|
|
31213
31972
|
}
|
|
31214
|
-
const hasExistingToolsRecord = isRecord(existingToolPermissions.tools);
|
|
31973
|
+
const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
|
|
31215
31974
|
const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
|
|
31216
31975
|
warnAboutDroppedOwnedKeys({
|
|
31217
31976
|
existingToolPermissions,
|
|
@@ -31255,7 +32014,7 @@ function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, gen
|
|
|
31255
32014
|
*/
|
|
31256
32015
|
function stripPermissiveOwnedValues(toolPermissions) {
|
|
31257
32016
|
const strippedKeys = [];
|
|
31258
|
-
if (isRecord(toolPermissions.tools)) {
|
|
32017
|
+
if (isRecord$1(toolPermissions.tools)) {
|
|
31259
32018
|
const tools = { ...toolPermissions.tools };
|
|
31260
32019
|
for (const toolKey of MANAGED_TOOL_KEYS) if (tools[toolKey] === "allow") {
|
|
31261
32020
|
delete tools[toolKey];
|
|
@@ -31277,14 +32036,14 @@ function stripPermissiveOwnedValues(toolPermissions) {
|
|
|
31277
32036
|
strippedKeys.push("default");
|
|
31278
32037
|
}
|
|
31279
32038
|
const bash = toolPermissions.bash;
|
|
31280
|
-
if (isRecord(bash)) {
|
|
32039
|
+
if (isRecord$1(bash)) {
|
|
31281
32040
|
const stripped = { ...bash };
|
|
31282
32041
|
if (stripped.default === "allow") {
|
|
31283
32042
|
delete stripped.default;
|
|
31284
32043
|
strippedKeys.push("bash.default");
|
|
31285
32044
|
}
|
|
31286
32045
|
if (Array.isArray(stripped.commands)) {
|
|
31287
|
-
const kept = stripped.commands.filter((entry) => !(isRecord(entry) && entry.permission === "allow"));
|
|
32046
|
+
const kept = stripped.commands.filter((entry) => !(isRecord$1(entry) && entry.permission === "allow"));
|
|
31288
32047
|
if (kept.length !== stripped.commands.length) strippedKeys.push("bash.commands");
|
|
31289
32048
|
if (kept.length > 0) stripped.commands = kept;
|
|
31290
32049
|
else delete stripped.commands;
|
|
@@ -31392,15 +32151,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
|
|
|
31392
32151
|
const permission = {};
|
|
31393
32152
|
if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
|
|
31394
32153
|
const bash = toolPermissions.bash;
|
|
31395
|
-
if (isRecord(bash)) {
|
|
32154
|
+
if (isRecord$1(bash)) {
|
|
31396
32155
|
const bashRules = {};
|
|
31397
32156
|
if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
|
|
31398
32157
|
if (Array.isArray(bash.commands)) {
|
|
31399
|
-
for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
32158
|
+
for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
31400
32159
|
}
|
|
31401
32160
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
31402
32161
|
}
|
|
31403
|
-
const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
|
|
32162
|
+
const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
|
|
31404
32163
|
const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
|
|
31405
32164
|
for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
|
|
31406
32165
|
const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
|
|
@@ -32115,11 +32874,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32115
32874
|
config,
|
|
32116
32875
|
logger
|
|
32117
32876
|
});
|
|
32118
|
-
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
32119
|
-
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
32877
|
+
const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
|
|
32878
|
+
const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
|
|
32120
32879
|
const override = config.warp;
|
|
32121
|
-
const executionProfileOverride = isRecord(override) && isRecord(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
|
|
32122
|
-
if (isRecord(override)) {
|
|
32880
|
+
const executionProfileOverride = isRecord$1(override) && isRecord$1(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
|
|
32881
|
+
if (isRecord$1(override)) {
|
|
32123
32882
|
const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
|
|
32124
32883
|
Object.assign(profiles, legacyOverride);
|
|
32125
32884
|
}
|
|
@@ -32154,10 +32913,10 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32154
32913
|
} catch (error) {
|
|
32155
32914
|
throw new Error(`Failed to parse Warp permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
32156
32915
|
}
|
|
32157
|
-
const agents = isRecord(settings.agents) ? settings.agents : {};
|
|
32158
|
-
const profiles = isRecord(agents.profiles) ? agents.profiles : {};
|
|
32159
|
-
const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
|
|
32160
|
-
const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
|
|
32916
|
+
const agents = isRecord$1(settings.agents) ? settings.agents : {};
|
|
32917
|
+
const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
|
|
32918
|
+
const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
|
|
32919
|
+
const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
|
|
32161
32920
|
const config = convertWarpToRulesyncPermissions({
|
|
32162
32921
|
allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
32163
32922
|
deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
@@ -32204,12 +32963,12 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32204
32963
|
*/
|
|
32205
32964
|
function mergeIntoDefaultExecutionProfile({ agents, mergedAllow, mergedDeny, executionProfileOverride, logger }) {
|
|
32206
32965
|
const hasOverrideKeys = executionProfileOverride !== void 0 && Object.keys(executionProfileOverride).length > 0;
|
|
32207
|
-
if (!isRecord(agents[EXECUTION_PROFILES_KEY])) {
|
|
32966
|
+
if (!isRecord$1(agents[EXECUTION_PROFILES_KEY])) {
|
|
32208
32967
|
if (hasOverrideKeys && logger) logger.warn("The warp.execution_profile permissions override was skipped: settings.toml has no [agents.execution_profiles] collection yet (un-migrated install). Open Warp once to run its settings migration, then re-run rulesync generate.");
|
|
32209
32968
|
return;
|
|
32210
32969
|
}
|
|
32211
32970
|
const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
|
|
32212
|
-
const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
|
|
32971
|
+
const defaultProfile = isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
|
|
32213
32972
|
if (executionProfileOverride) Object.assign(defaultProfile, executionProfileOverride);
|
|
32214
32973
|
if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
|
|
32215
32974
|
else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
|
|
@@ -32283,9 +33042,13 @@ const ZedToolPermissionsSchema = zod_mini.z.looseObject({
|
|
|
32283
33042
|
default: zod_mini.z.optional(ZedPermissionActionSchema),
|
|
32284
33043
|
tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), ZedToolPermissionSchema))
|
|
32285
33044
|
});
|
|
33045
|
+
/** Canonical per-tool MCP category prefix: `mcp__<server>__<tool>`. */
|
|
33046
|
+
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
33047
|
+
/** Zed's per-tool MCP name prefix: `mcp:<server>:<tool>`. */
|
|
33048
|
+
const MCP_ZED_PREFIX = "mcp:";
|
|
32286
33049
|
/**
|
|
32287
33050
|
* Mapping from rulesync canonical tool category names to Zed agent tool names.
|
|
32288
|
-
* Unknown names are passed through as-is
|
|
33051
|
+
* Unknown names are passed through as-is.
|
|
32289
33052
|
*/
|
|
32290
33053
|
const CANONICAL_TO_ZED_TOOL_NAMES = {
|
|
32291
33054
|
bash: "terminal",
|
|
@@ -32295,13 +33058,93 @@ const CANONICAL_TO_ZED_TOOL_NAMES = {
|
|
|
32295
33058
|
webfetch: "fetch",
|
|
32296
33059
|
websearch: "search_web"
|
|
32297
33060
|
};
|
|
33061
|
+
/**
|
|
33062
|
+
* Canonical categories whose Zed tool is not permission-gated. Zed's gated list
|
|
33063
|
+
* is `terminal`, `edit_file`, `write_file`, `delete_path`, `move_path`,
|
|
33064
|
+
* `copy_path`, `create_directory`, `fetch`, `search_web` and `skill`; the
|
|
33065
|
+
* read-only tools (`read_file`, `grep`, `find_path`, `list_directory`) sit in
|
|
33066
|
+
* Zed's own `EXCLUDED_TOOLS` and never call `decide_permission_from_settings`,
|
|
33067
|
+
* so a `tools.<name>` entry for one is config Zed never consults. Zed's real
|
|
33068
|
+
* read-denial surface is `private_files`, which the ignore feature owns.
|
|
33069
|
+
*
|
|
33070
|
+
* @see https://zed.dev/docs/ai/tool-permissions#supported-tools
|
|
33071
|
+
*/
|
|
33072
|
+
const ZED_EXCLUDED_CANONICAL_CATEGORIES = /* @__PURE__ */ new Set([
|
|
33073
|
+
"read",
|
|
33074
|
+
"grep",
|
|
33075
|
+
"glob"
|
|
33076
|
+
]);
|
|
33077
|
+
/** The Zed-side spellings of the same tools, for a category that names one directly. */
|
|
33078
|
+
const ZED_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
33079
|
+
"read_file",
|
|
33080
|
+
"grep",
|
|
33081
|
+
"find_path",
|
|
33082
|
+
"list_directory"
|
|
33083
|
+
]);
|
|
33084
|
+
const isZedExcludedCategory = (category) => ZED_EXCLUDED_CANONICAL_CATEGORIES.has(category) || ZED_EXCLUDED_TOOL_NAMES.has(toZedToolName(category));
|
|
32298
33085
|
const ZED_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZED_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
33086
|
+
/**
|
|
33087
|
+
* Zed addresses an MCP tool as `mcp:<server>:<tool>`, where rulesync's canonical
|
|
33088
|
+
* category is `mcp__<server>__<tool>`. Without this translation the canonical
|
|
33089
|
+
* spelling lands under a key Zed never looks up.
|
|
33090
|
+
*
|
|
33091
|
+
* Only the FIRST separator is split, matching the `cursor-permissions.ts`
|
|
33092
|
+
* precedent: upstream's `mcp_tool_id` concatenates the two names without
|
|
33093
|
+
* escaping either, so a tool called `create__issue` is legitimately
|
|
33094
|
+
* `mcp:github:create__issue`. Splitting every separator would rewrite that into
|
|
33095
|
+
* a third key on the next generate.
|
|
33096
|
+
*
|
|
33097
|
+
* @see https://zed.dev/docs/ai/tool-permissions
|
|
33098
|
+
*/
|
|
32299
33099
|
function toZedToolName(canonical) {
|
|
33100
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
33101
|
+
const [server, ...toolParts] = canonical.slice(5).split("__");
|
|
33102
|
+
const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
|
|
33103
|
+
return `${MCP_ZED_PREFIX}${address}`;
|
|
33104
|
+
}
|
|
32300
33105
|
return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
|
|
32301
33106
|
}
|
|
32302
33107
|
function toCanonicalToolName(zedName) {
|
|
33108
|
+
if (zedName.startsWith(MCP_ZED_PREFIX)) {
|
|
33109
|
+
const [server, ...toolParts] = zedName.slice(4).split(":");
|
|
33110
|
+
const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
|
|
33111
|
+
return `${MCP_CANONICAL_PREFIX}${address}`;
|
|
33112
|
+
}
|
|
32303
33113
|
return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
|
|
32304
33114
|
}
|
|
33115
|
+
/**
|
|
33116
|
+
* Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
|
|
33117
|
+
* tool's text input, and it dispatches every MCP tool with a single empty input
|
|
33118
|
+
* (`&[String::new()]`, commented upstream as "MCP tools are gated only by tool
|
|
33119
|
+
* id (no per-input pattern matching)"). The regexes still run, but against `""`,
|
|
33120
|
+
* so a pattern-scoped rule silently does something other than what its author
|
|
33121
|
+
* meant — and a non-matching `always_allow` downgrades the outcome to confirm.
|
|
33122
|
+
* Only the category's `*` rule (Zed's per-tool `default`) is therefore emitted.
|
|
33123
|
+
*/
|
|
33124
|
+
const isMcpZedToolName = (zedName) => zedName.startsWith(MCP_ZED_PREFIX);
|
|
33125
|
+
/**
|
|
33126
|
+
* Zed looks a tool up by exact key on the full `mcp:<server>:<tool>` triple, so
|
|
33127
|
+
* an address is inert unless it names both a concrete server and a concrete
|
|
33128
|
+
* tool: a missing half matches nothing, and so does a wildcard, since Zed does
|
|
33129
|
+
* no glob or prefix matching on the key.
|
|
33130
|
+
*/
|
|
33131
|
+
function isInertMcpAddress(zedToolName) {
|
|
33132
|
+
if (!isMcpZedToolName(zedToolName)) return false;
|
|
33133
|
+
const [server, ...toolParts] = zedToolName.slice(4).split(":");
|
|
33134
|
+
const tool = toolParts.join(":");
|
|
33135
|
+
return !server || server === "*" || !tool || tool === "*";
|
|
33136
|
+
}
|
|
33137
|
+
/**
|
|
33138
|
+
* Strip pattern-scoped rules from an MCP category, warning once about the ones
|
|
33139
|
+
* dropped. Non-MCP categories are returned untouched.
|
|
33140
|
+
*/
|
|
33141
|
+
function withoutInertMcpPatterns({ category, zedToolName, rules, logger }) {
|
|
33142
|
+
if (!isMcpZedToolName(zedToolName)) return rules;
|
|
33143
|
+
const scopedPatterns = Object.keys(rules).filter((pattern) => pattern !== "*");
|
|
33144
|
+
if (scopedPatterns.length === 0) return rules;
|
|
33145
|
+
logger?.warn(`Zed permissions: dropping the pattern-scoped rule(s) ${scopedPatterns.map((pattern) => `"${pattern}"`).join(", ")} in the "${category}" category — Zed dispatches an MCP tool with an empty input, so a permission pattern is matched against "" rather than against anything meaningful. Only the catch-all "*" rule is emitted, as the tool's default.`);
|
|
33146
|
+
return Object.fromEntries(Object.entries(rules).filter(([pattern]) => pattern === "*"));
|
|
33147
|
+
}
|
|
32305
33148
|
const CANONICAL_TO_ZED_ACTION = {
|
|
32306
33149
|
allow: "allow",
|
|
32307
33150
|
ask: "confirm",
|
|
@@ -32344,6 +33187,53 @@ function buildZedToolPermission(rules) {
|
|
|
32344
33187
|
if (alwaysConfirm.length > 0) tool.always_confirm = alwaysConfirm;
|
|
32345
33188
|
return Object.keys(tool).length > 0 ? tool : null;
|
|
32346
33189
|
}
|
|
33190
|
+
/**
|
|
33191
|
+
* Split a canonical permission block into the Zed shapes it maps onto, plus the
|
|
33192
|
+
* categories the caller should report as dropped.
|
|
33193
|
+
*
|
|
33194
|
+
* The canonical `*` category is the all-tools catch-all. Zed's counterpart is
|
|
33195
|
+
* `agent.tool_permissions.default` (rung 6 of its precedence ladder), not a
|
|
33196
|
+
* `tools["*"]` entry — `*` is not a Zed tool name, so writing one produces a
|
|
33197
|
+
* rule Zed silently ignores. Only the category's own `*` pattern can be
|
|
33198
|
+
* expressed there: Zed's global default carries no pattern list, so
|
|
33199
|
+
* pattern-scoped rules in the `*` category are dropped with a warning instead of
|
|
33200
|
+
* being emitted as inert config.
|
|
33201
|
+
*/
|
|
33202
|
+
function buildZedToolPermissions({ permission, logger }) {
|
|
33203
|
+
let managedDefault;
|
|
33204
|
+
const managedTools = {};
|
|
33205
|
+
const excludedCategories = [];
|
|
33206
|
+
const inertMcpCategories = [];
|
|
33207
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
33208
|
+
if (category === "*") {
|
|
33209
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
33210
|
+
else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
|
|
33211
|
+
continue;
|
|
33212
|
+
}
|
|
33213
|
+
if (isZedExcludedCategory(category)) {
|
|
33214
|
+
if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
|
|
33215
|
+
continue;
|
|
33216
|
+
}
|
|
33217
|
+
const zedToolName = toZedToolName(category);
|
|
33218
|
+
if (isInertMcpAddress(zedToolName)) {
|
|
33219
|
+
inertMcpCategories.push(category);
|
|
33220
|
+
continue;
|
|
33221
|
+
}
|
|
33222
|
+
const tool = buildZedToolPermission(withoutInertMcpPatterns({
|
|
33223
|
+
category,
|
|
33224
|
+
zedToolName,
|
|
33225
|
+
rules,
|
|
33226
|
+
logger
|
|
33227
|
+
}));
|
|
33228
|
+
if (tool) managedTools[zedToolName] = tool;
|
|
33229
|
+
}
|
|
33230
|
+
return {
|
|
33231
|
+
managedDefault,
|
|
33232
|
+
managedTools,
|
|
33233
|
+
excludedCategories,
|
|
33234
|
+
inertMcpCategories
|
|
33235
|
+
};
|
|
33236
|
+
}
|
|
32347
33237
|
function asRecord(value) {
|
|
32348
33238
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
|
|
32349
33239
|
return Object.fromEntries(Object.entries(value));
|
|
@@ -32414,19 +33304,15 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
32414
33304
|
const agent = asRecord(settings.agent);
|
|
32415
33305
|
const toolPermissions = asRecord(agent.tool_permissions);
|
|
32416
33306
|
const existingTools = asRecord(toolPermissions.tools);
|
|
32417
|
-
|
|
32418
|
-
|
|
32419
|
-
|
|
32420
|
-
|
|
32421
|
-
|
|
32422
|
-
|
|
32423
|
-
continue;
|
|
32424
|
-
}
|
|
32425
|
-
const tool = buildZedToolPermission(rules);
|
|
32426
|
-
if (tool) managedTools[toZedToolName(category)] = tool;
|
|
32427
|
-
}
|
|
33307
|
+
const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
|
|
33308
|
+
permission: config.permission,
|
|
33309
|
+
logger
|
|
33310
|
+
});
|
|
33311
|
+
if (excludedCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${excludedCategories.map((category) => `"${category}"`).join(", ")} ${excludedCategories.length === 1 ? "category" : "categories"} — Zed does not gate its read-only tools, so the entries would never be consulted. Zed's read-denial surface is \`private_files\`, which the ignore feature writes from \`.rulesync/.aiignore\`.`);
|
|
33312
|
+
if (inertMcpCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${inertMcpCategories.map((category) => `"${category}"`).join(", ")} ${inertMcpCategories.length === 1 ? "category" : "categories"} — Zed looks an MCP tool up by exact key on the full \`mcp:<server>:<tool>\` triple, so an address that omits or wildcards either half matches nothing. Name the individual server and tool instead.`);
|
|
32428
33313
|
const managedToolNames = new Set(Object.keys(managedTools));
|
|
32429
33314
|
if ("*" in config.permission) managedToolNames.add("*");
|
|
33315
|
+
for (const toolName of Object.keys(existingTools)) if (toolName.startsWith(MCP_CANONICAL_PREFIX)) managedToolNames.add(toolName);
|
|
32430
33316
|
const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
|
|
32431
33317
|
return new ZedPermissions({
|
|
32432
33318
|
outputRoot,
|
|
@@ -33216,18 +34102,16 @@ var DirFeatureProcessor = class {
|
|
|
33216
34102
|
})) dirHasChanges = true;
|
|
33217
34103
|
}
|
|
33218
34104
|
const otherFiles = aiDir.getOtherFiles();
|
|
33219
|
-
const otherFileContents = [];
|
|
33220
34105
|
for (const file of otherFiles) {
|
|
33221
|
-
|
|
33222
|
-
|
|
33223
|
-
|
|
33224
|
-
|
|
33225
|
-
|
|
33226
|
-
|
|
33227
|
-
|
|
33228
|
-
|
|
33229
|
-
|
|
33230
|
-
}
|
|
34106
|
+
if (dirHasChanges) break;
|
|
34107
|
+
const filePath = (0, node_path.join)(dirPath, file.relativeFilePathToDirPath);
|
|
34108
|
+
const existingBuffer = await readFileBufferOrNull(filePath);
|
|
34109
|
+
if (!companionFileContentsEquivalent({
|
|
34110
|
+
filePath,
|
|
34111
|
+
expected: file.fileBuffer,
|
|
34112
|
+
existing: existingBuffer,
|
|
34113
|
+
composed: file.composed
|
|
34114
|
+
})) dirHasChanges = true;
|
|
33231
34115
|
}
|
|
33232
34116
|
if (!dirHasChanges) continue;
|
|
33233
34117
|
const relativeDir = aiDir.getRelativePathFromCwd();
|
|
@@ -33247,11 +34131,8 @@ var DirFeatureProcessor = class {
|
|
|
33247
34131
|
await writeFileContent((0, node_path.join)(dirPath, mainFile.name), mainFileContent);
|
|
33248
34132
|
changedPaths.push((0, node_path.join)(relativeDir, mainFile.name));
|
|
33249
34133
|
}
|
|
33250
|
-
for (const
|
|
33251
|
-
|
|
33252
|
-
const content = otherFileContents[i];
|
|
33253
|
-
if (content === void 0) throw new Error(`Internal error: content for file ${file.relativeFilePathToDirPath} is undefined. This indicates a synchronization issue between otherFiles and otherFileContents arrays.`);
|
|
33254
|
-
await writeFileContent(filePath, content);
|
|
34134
|
+
for (const file of otherFiles) {
|
|
34135
|
+
await writeFileBuffer((0, node_path.join)(dirPath, file.relativeFilePathToDirPath), file.fileBuffer);
|
|
33255
34136
|
changedPaths.push((0, node_path.join)(relativeDir, file.relativeFilePathToDirPath));
|
|
33256
34137
|
}
|
|
33257
34138
|
}
|
|
@@ -33977,6 +34858,39 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
33977
34858
|
return frontmatter;
|
|
33978
34859
|
}
|
|
33979
34860
|
/**
|
|
34861
|
+
* Escapes the glob metacharacters in a directory path so it matches literally.
|
|
34862
|
+
* A real directory name may contain them — `app/[slug]` in a Next.js tree is
|
|
34863
|
+
* the common case, and unescaped `[slug]` reads as a bracket expression that
|
|
34864
|
+
* matches a different subtree (or nothing at all).
|
|
34865
|
+
*
|
|
34866
|
+
* @see https://code.claude.com/docs/en/memory
|
|
34867
|
+
*/
|
|
34868
|
+
function escapeGlobLiteral(dirPath) {
|
|
34869
|
+
return dirPath.replaceAll(/[\\*?[\]{}()!]/g, "\\$&");
|
|
34870
|
+
}
|
|
34871
|
+
/**
|
|
34872
|
+
* Claude Code scopes a nested skill by its location: a skill living in
|
|
34873
|
+
* `apps/web/.claude/skills/deploy` only activates while working under
|
|
34874
|
+
* `apps/web`. rulesync generates every imported skill into the project-root
|
|
34875
|
+
* `.claude/skills/`, so on import that location-based scoping has to be
|
|
34876
|
+
* re-expressed as an explicit `paths` glob — otherwise the round-trip silently
|
|
34877
|
+
* promotes a subtree skill to global activation.
|
|
34878
|
+
*
|
|
34879
|
+
* Returns the derived glob for a nested discovery root, or `undefined` for the
|
|
34880
|
+
* project-root `.claude/skills` (and for any root whose subtree cannot be
|
|
34881
|
+
* determined), where no scoping is implied.
|
|
34882
|
+
*
|
|
34883
|
+
* @see https://code.claude.com/docs/en/skills
|
|
34884
|
+
*/
|
|
34885
|
+
function deriveNestedSkillPaths(relativeDirPath) {
|
|
34886
|
+
const posixDirPath = toPosixPath(relativeDirPath);
|
|
34887
|
+
const skillsDirSuffix = `/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`;
|
|
34888
|
+
if (!posixDirPath.endsWith(skillsDirSuffix)) return;
|
|
34889
|
+
const subtree = posixDirPath.slice(0, -skillsDirSuffix.length);
|
|
34890
|
+
if (subtree === "" || subtree === ".") return;
|
|
34891
|
+
return [`${escapeGlobLiteral(subtree)}/**`];
|
|
34892
|
+
}
|
|
34893
|
+
/**
|
|
33980
34894
|
* Represents a Claude Code skill directory.
|
|
33981
34895
|
* Unlike subagents and commands, skills are directories containing SKILL.md and other files.
|
|
33982
34896
|
* Extends ToolSkill to inherit directory management and security features from AiDir.
|
|
@@ -34064,6 +34978,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34064
34978
|
}
|
|
34065
34979
|
toRulesyncSkill() {
|
|
34066
34980
|
const frontmatter = this.getFrontmatter();
|
|
34981
|
+
const resolvedPaths = frontmatter.paths !== void 0 ? frontmatter.paths : deriveNestedSkillPaths(this.relativeDirPath);
|
|
34067
34982
|
const claudecodeSection = {
|
|
34068
34983
|
...frontmatter.when_to_use && { when_to_use: frontmatter.when_to_use },
|
|
34069
34984
|
...frontmatter["allowed-tools"] && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
@@ -34080,7 +34995,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34080
34995
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34081
34996
|
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34082
34997
|
...this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true },
|
|
34083
|
-
...
|
|
34998
|
+
...resolvedPaths !== void 0 && { paths: resolvedPaths }
|
|
34084
34999
|
};
|
|
34085
35000
|
const rulesyncFrontmatter = {
|
|
34086
35001
|
name: frontmatter.name,
|
|
@@ -34478,7 +35393,8 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
34478
35393
|
fileBuffer: Buffer.from((0, js_yaml.dump)(openaiObject, {
|
|
34479
35394
|
lineWidth: -1,
|
|
34480
35395
|
noRefs: true
|
|
34481
|
-
}))
|
|
35396
|
+
})),
|
|
35397
|
+
composed: true
|
|
34482
35398
|
}] : baseOtherFiles;
|
|
34483
35399
|
return new CodexCliSkill({
|
|
34484
35400
|
outputRoot,
|
|
@@ -34538,7 +35454,11 @@ const CopilotSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
34538
35454
|
name: zod_mini.z.string(),
|
|
34539
35455
|
description: zod_mini.z.string(),
|
|
34540
35456
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
34541
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
35457
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
35458
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string()),
|
|
35459
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
35460
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
35461
|
+
context: zod_mini.z.optional(zod_mini.z.string())
|
|
34542
35462
|
});
|
|
34543
35463
|
/**
|
|
34544
35464
|
* Represents a GitHub Copilot skill directory.
|
|
@@ -34593,14 +35513,10 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34593
35513
|
};
|
|
34594
35514
|
}
|
|
34595
35515
|
toRulesyncSkill() {
|
|
34596
|
-
const
|
|
34597
|
-
const copilotSection = {
|
|
34598
|
-
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
34599
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
34600
|
-
};
|
|
35516
|
+
const { name, description, ...copilotSection } = this.getFrontmatter();
|
|
34601
35517
|
const rulesyncFrontmatter = {
|
|
34602
|
-
name
|
|
34603
|
-
description
|
|
35518
|
+
name,
|
|
35519
|
+
description,
|
|
34604
35520
|
targets: ["*"],
|
|
34605
35521
|
...Object.keys(copilotSection).length > 0 && { copilot: copilotSection }
|
|
34606
35522
|
};
|
|
@@ -34618,11 +35534,22 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34618
35534
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34619
35535
|
const settablePaths = CopilotSkill.getSettablePaths({ global });
|
|
34620
35536
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35537
|
+
const copilotSection = rulesyncFrontmatter.copilot;
|
|
35538
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
35539
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35540
|
+
section: copilotSection
|
|
35541
|
+
});
|
|
35542
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35543
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35544
|
+
section: copilotSection
|
|
35545
|
+
});
|
|
35546
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotFields } = copilotSection ?? {};
|
|
34621
35547
|
const copilotFrontmatter = {
|
|
35548
|
+
...copilotFields,
|
|
34622
35549
|
name: rulesyncFrontmatter.name,
|
|
34623
35550
|
description: rulesyncFrontmatter.description,
|
|
34624
|
-
...
|
|
34625
|
-
...
|
|
35551
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35552
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
34626
35553
|
};
|
|
34627
35554
|
return new CopilotSkill({
|
|
34628
35555
|
outputRoot,
|
|
@@ -34741,17 +35668,10 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
34741
35668
|
};
|
|
34742
35669
|
}
|
|
34743
35670
|
toRulesyncSkill() {
|
|
34744
|
-
const
|
|
34745
|
-
const copilotcliSection = {
|
|
34746
|
-
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
34747
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
34748
|
-
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
|
|
34749
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34750
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
|
|
34751
|
-
};
|
|
35671
|
+
const { name, description, ...copilotcliSection } = this.getFrontmatter();
|
|
34752
35672
|
const rulesyncFrontmatter = {
|
|
34753
|
-
name
|
|
34754
|
-
description
|
|
35673
|
+
name,
|
|
35674
|
+
description,
|
|
34755
35675
|
targets: ["*"],
|
|
34756
35676
|
...Object.keys(copilotcliSection).length > 0 && { copilotcli: copilotcliSection }
|
|
34757
35677
|
};
|
|
@@ -34769,14 +35689,22 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
34769
35689
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34770
35690
|
const settablePaths = CopilotcliSkill.getSettablePaths({ global });
|
|
34771
35691
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35692
|
+
const copilotcliSection = rulesyncFrontmatter.copilotcli;
|
|
35693
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
35694
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35695
|
+
section: copilotcliSection
|
|
35696
|
+
});
|
|
35697
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35698
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35699
|
+
section: copilotcliSection
|
|
35700
|
+
});
|
|
35701
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotcliFields } = copilotcliSection ?? {};
|
|
34772
35702
|
const copilotcliFrontmatter = {
|
|
35703
|
+
...copilotcliFields,
|
|
34773
35704
|
name: rulesyncFrontmatter.name,
|
|
34774
35705
|
description: rulesyncFrontmatter.description,
|
|
34775
|
-
...
|
|
34776
|
-
...
|
|
34777
|
-
...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] },
|
|
34778
|
-
...rulesyncFrontmatter.copilotcli?.["user-invocable"] !== void 0 && { "user-invocable": rulesyncFrontmatter.copilotcli["user-invocable"] },
|
|
34779
|
-
...rulesyncFrontmatter.copilotcli?.["disable-model-invocation"] !== void 0 && { "disable-model-invocation": rulesyncFrontmatter.copilotcli["disable-model-invocation"] }
|
|
35706
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35707
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
34780
35708
|
};
|
|
34781
35709
|
return new CopilotcliSkill({
|
|
34782
35710
|
outputRoot,
|
|
@@ -35334,7 +36262,9 @@ const FactorydroidSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
35334
36262
|
name: zod_mini.z.string(),
|
|
35335
36263
|
description: zod_mini.z.string(),
|
|
35336
36264
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
35337
|
-
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
|
|
36265
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
36266
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
36267
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
35338
36268
|
});
|
|
35339
36269
|
/**
|
|
35340
36270
|
* Represents a Factory Droid skill directory.
|
|
@@ -35390,7 +36320,9 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35390
36320
|
const frontmatter = this.getFrontmatter();
|
|
35391
36321
|
const factorydroidBlock = {
|
|
35392
36322
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
35393
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] }
|
|
36323
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
36324
|
+
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
36325
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
35394
36326
|
};
|
|
35395
36327
|
const rulesyncFrontmatter = {
|
|
35396
36328
|
name: frontmatter.name,
|
|
@@ -35412,19 +36344,22 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35412
36344
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
35413
36345
|
const settablePaths = FactorydroidSkill.getSettablePaths({ global });
|
|
35414
36346
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
36347
|
+
const factorydroidSection = rulesyncFrontmatter.factorydroid;
|
|
35415
36348
|
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35416
36349
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35417
|
-
section:
|
|
36350
|
+
section: factorydroidSection
|
|
35418
36351
|
});
|
|
35419
36352
|
const resolvedUserInvocable = resolveUserInvocable({
|
|
35420
36353
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35421
|
-
section:
|
|
36354
|
+
section: factorydroidSection
|
|
35422
36355
|
});
|
|
35423
36356
|
const factorydroidFrontmatter = {
|
|
35424
36357
|
name: rulesyncFrontmatter.name,
|
|
35425
36358
|
description: rulesyncFrontmatter.description,
|
|
35426
36359
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
35427
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
36360
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
36361
|
+
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
36362
|
+
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
35428
36363
|
};
|
|
35429
36364
|
return new FactorydroidSkill({
|
|
35430
36365
|
outputRoot,
|
|
@@ -36241,7 +37176,10 @@ var KimiCodeSkill = class KimiCodeSkill extends ToolSkill {
|
|
|
36241
37176
|
//#region src/features/skills/kiro-skill.ts
|
|
36242
37177
|
const KiroSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
36243
37178
|
name: zod_mini.z.string(),
|
|
36244
|
-
description: zod_mini.z.string()
|
|
37179
|
+
description: zod_mini.z.string(),
|
|
37180
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
37181
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
37182
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
36245
37183
|
});
|
|
36246
37184
|
/**
|
|
36247
37185
|
* Represents a Kiro skill directory.
|
|
@@ -36295,11 +37233,12 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
36295
37233
|
};
|
|
36296
37234
|
}
|
|
36297
37235
|
toRulesyncSkill() {
|
|
36298
|
-
const
|
|
37236
|
+
const { name, description, ...kiroSection } = this.getFrontmatter();
|
|
36299
37237
|
const rulesyncFrontmatter = {
|
|
36300
|
-
name
|
|
36301
|
-
description
|
|
36302
|
-
targets: ["*"]
|
|
37238
|
+
name,
|
|
37239
|
+
description,
|
|
37240
|
+
targets: ["*"],
|
|
37241
|
+
...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
|
|
36303
37242
|
};
|
|
36304
37243
|
return new RulesyncSkill({
|
|
36305
37244
|
outputRoot: this.outputRoot,
|
|
@@ -36316,6 +37255,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
36316
37255
|
const settablePaths = KiroSkill.getSettablePaths({ global });
|
|
36317
37256
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
36318
37257
|
const kiroFrontmatter = {
|
|
37258
|
+
...rulesyncFrontmatter.kiro,
|
|
36319
37259
|
name: rulesyncFrontmatter.name,
|
|
36320
37260
|
description: rulesyncFrontmatter.description
|
|
36321
37261
|
};
|
|
@@ -39445,7 +40385,17 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39445
40385
|
validate: true
|
|
39446
40386
|
});
|
|
39447
40387
|
}
|
|
39448
|
-
|
|
40388
|
+
/**
|
|
40389
|
+
* Last chance to adjust the tool frontmatter before it is written. The base
|
|
40390
|
+
* implementation only warns about names Claude Code rejects; plugin-scoped
|
|
40391
|
+
* subclasses extend it to drop fields Claude Code refuses to honor for
|
|
40392
|
+
* plugin-shipped agents.
|
|
40393
|
+
*/
|
|
40394
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
40395
|
+
if (frontmatter.name.includes(":")) logger?.warn(`Claude Code will reject the subagent in ${relativeFilePath}: the name "${frontmatter.name}" contains ":", which is reserved for plugin namespacing.`);
|
|
40396
|
+
return frontmatter;
|
|
40397
|
+
}
|
|
40398
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
39449
40399
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
39450
40400
|
const claudecodeSection = this.filterToolSpecificSection(rulesyncFrontmatter.claudecode ?? {}, ["name", "description"]);
|
|
39451
40401
|
const rawClaudecodeFrontmatter = {
|
|
@@ -39455,7 +40405,11 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39455
40405
|
};
|
|
39456
40406
|
const result = ClaudecodeSubagentFrontmatterSchema.safeParse(rawClaudecodeFrontmatter);
|
|
39457
40407
|
if (!result.success) throw new Error(`Invalid claudecode subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
39458
|
-
const claudecodeFrontmatter =
|
|
40408
|
+
const claudecodeFrontmatter = this.sanitizeFrontmatter({
|
|
40409
|
+
frontmatter: result.data,
|
|
40410
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
40411
|
+
logger
|
|
40412
|
+
});
|
|
39459
40413
|
const body = rulesyncSubagent.getBody();
|
|
39460
40414
|
const fileContent = stringifyFrontmatter(body, claudecodeFrontmatter);
|
|
39461
40415
|
const paths = this.getSettablePaths({ global });
|
|
@@ -39524,6 +40478,21 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39524
40478
|
};
|
|
39525
40479
|
//#endregion
|
|
39526
40480
|
//#region src/features/subagents/claudecode-plugin-subagent.ts
|
|
40481
|
+
/**
|
|
40482
|
+
* Claude Code refuses these for plugin-shipped agents "for security reasons",
|
|
40483
|
+
* so emitting them leaves the author believing the agent is constrained when it
|
|
40484
|
+
* is not. Only these three are dropped: the other fields upstream does not list
|
|
40485
|
+
* (e.g. `color`) are merely ignored, with no misleading security posture.
|
|
40486
|
+
*
|
|
40487
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
40488
|
+
*/
|
|
40489
|
+
const PLUGIN_FORBIDDEN_FIELDS = [
|
|
40490
|
+
"hooks",
|
|
40491
|
+
"mcpServers",
|
|
40492
|
+
"permissionMode"
|
|
40493
|
+
];
|
|
40494
|
+
/** The only `isolation` value plugin agents accept. */
|
|
40495
|
+
const PLUGIN_ISOLATION_VALUE = "worktree";
|
|
39527
40496
|
var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
39528
40497
|
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
39529
40498
|
const targets = rulesyncSubagent.getFrontmatter().targets;
|
|
@@ -39532,6 +40501,21 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
|
39532
40501
|
static getSettablePaths() {
|
|
39533
40502
|
return { relativeDirPath: CLAUDECODE_PLUGIN_AGENTS_DIR };
|
|
39534
40503
|
}
|
|
40504
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
40505
|
+
const sanitized = { ...super.sanitizeFrontmatter({
|
|
40506
|
+
frontmatter,
|
|
40507
|
+
relativeFilePath,
|
|
40508
|
+
logger
|
|
40509
|
+
}) };
|
|
40510
|
+
const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
|
|
40511
|
+
for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
|
|
40512
|
+
if (dropped.length > 0) logger?.warn(`Dropping ${dropped.join(", ")} from claudecode-plugin subagent ${relativeFilePath}: Claude Code does not support these fields for plugin-shipped agents.`);
|
|
40513
|
+
if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
|
|
40514
|
+
logger?.warn(`Dropping isolation "${sanitized.isolation}" from claudecode-plugin subagent ${relativeFilePath}: "${PLUGIN_ISOLATION_VALUE}" is the only value Claude Code accepts for plugin-shipped agents.`);
|
|
40515
|
+
delete sanitized.isolation;
|
|
40516
|
+
}
|
|
40517
|
+
return sanitized;
|
|
40518
|
+
}
|
|
39535
40519
|
};
|
|
39536
40520
|
//#endregion
|
|
39537
40521
|
//#region src/features/subagents/cline-subagent.ts
|
|
@@ -42840,14 +43824,14 @@ var ZoocodeSubagent = class extends RooSubagent {
|
|
|
42840
43824
|
static toRooMode(rulesyncSubagent) {
|
|
42841
43825
|
const mode = super.toRooMode(rulesyncSubagent);
|
|
42842
43826
|
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
42843
|
-
const zoocodeSection = isRecord(frontmatter.zoocode) ? frontmatter.zoocode : {};
|
|
43827
|
+
const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
|
|
42844
43828
|
if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
|
|
42845
43829
|
return mode;
|
|
42846
43830
|
}
|
|
42847
43831
|
toRulesyncSubagents() {
|
|
42848
43832
|
return super.toRulesyncSubagents().map((subagent) => {
|
|
42849
43833
|
const frontmatter = subagent.getFrontmatter();
|
|
42850
|
-
const { allowedMcpServers, ...restRooSection } = isRecord(frontmatter.roo) ? { ...frontmatter.roo } : {};
|
|
43834
|
+
const { allowedMcpServers, ...restRooSection } = isRecord$1(frontmatter.roo) ? { ...frontmatter.roo } : {};
|
|
42851
43835
|
const rebuilt = {
|
|
42852
43836
|
...frontmatter,
|
|
42853
43837
|
targets: ["zoocode"],
|
|
@@ -43185,7 +44169,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
43185
44169
|
outputRoot: this.outputRoot,
|
|
43186
44170
|
relativeDirPath: RulesyncSubagent.getSettablePaths().relativeDirPath,
|
|
43187
44171
|
rulesyncSubagent,
|
|
43188
|
-
global: this.global
|
|
44172
|
+
global: this.global,
|
|
44173
|
+
logger: this.logger
|
|
43189
44174
|
}));
|
|
43190
44175
|
}
|
|
43191
44176
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
@@ -45138,7 +46123,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
|
45138
46123
|
};
|
|
45139
46124
|
//#endregion
|
|
45140
46125
|
//#region src/features/rules/copilot-rule.ts
|
|
45141
|
-
const CopilotRuleFrontmatterSchema = zod_mini.z.
|
|
46126
|
+
const CopilotRuleFrontmatterSchema = zod_mini.z.looseObject({
|
|
45142
46127
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
45143
46128
|
applyTo: zod_mini.z.optional(zod_mini.z.string()),
|
|
45144
46129
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -45194,15 +46179,13 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45194
46179
|
toRulesyncRule() {
|
|
45195
46180
|
let globs;
|
|
45196
46181
|
if (this.frontmatter.applyTo) globs = this.frontmatter.applyTo.split(",").map((g) => g.trim());
|
|
46182
|
+
const { description, applyTo: _applyTo, ...copilotFields } = this.frontmatter;
|
|
45197
46183
|
const rulesyncFrontmatter = {
|
|
45198
46184
|
targets: ["*"],
|
|
45199
46185
|
root: this.isRoot(),
|
|
45200
|
-
description
|
|
46186
|
+
description,
|
|
45201
46187
|
globs,
|
|
45202
|
-
...(
|
|
45203
|
-
...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
|
|
45204
|
-
...this.frontmatter.name && { name: this.frontmatter.name }
|
|
45205
|
-
} }
|
|
46188
|
+
...Object.keys(copilotFields).length > 0 && { copilot: copilotFields }
|
|
45206
46189
|
};
|
|
45207
46190
|
const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
|
|
45208
46191
|
return new RulesyncRule({
|
|
@@ -45219,10 +46202,9 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45219
46202
|
const root = rulesyncFrontmatter.root;
|
|
45220
46203
|
const paths = this.getSettablePaths({ global });
|
|
45221
46204
|
const copilotFrontmatter = {
|
|
46205
|
+
...rulesyncFrontmatter.copilot,
|
|
45222
46206
|
description: rulesyncFrontmatter.description,
|
|
45223
|
-
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45224
|
-
excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
|
|
45225
|
-
name: rulesyncFrontmatter.copilot?.name
|
|
46207
|
+
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45226
46208
|
};
|
|
45227
46209
|
const body = rulesyncRule.getBody();
|
|
45228
46210
|
if (root) return new CopilotRule({
|
|
@@ -51621,6 +52603,12 @@ Object.defineProperty(exports, "warnOnConflictingFlags", {
|
|
|
51621
52603
|
return warnOnConflictingFlags;
|
|
51622
52604
|
}
|
|
51623
52605
|
});
|
|
52606
|
+
Object.defineProperty(exports, "writeFileBuffer", {
|
|
52607
|
+
enumerable: true,
|
|
52608
|
+
get: function() {
|
|
52609
|
+
return writeFileBuffer;
|
|
52610
|
+
}
|
|
52611
|
+
});
|
|
51624
52612
|
Object.defineProperty(exports, "writeFileContent", {
|
|
51625
52613
|
enumerable: true,
|
|
51626
52614
|
get: function() {
|