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
|
@@ -614,6 +614,13 @@ async function readFileBuffer(filepath) {
|
|
|
614
614
|
return readFile(filepath);
|
|
615
615
|
}
|
|
616
616
|
/**
|
|
617
|
+
* Read file as a buffer if it exists, otherwise return null.
|
|
618
|
+
*/
|
|
619
|
+
async function readFileBufferOrNull(filepath) {
|
|
620
|
+
if (await fileExists(filepath)) return readFileBuffer(filepath);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
617
624
|
* Normalizes text to LF line endings and adds exactly one trailing newline.
|
|
618
625
|
* Removes any existing trailing whitespace and appends a single newline.
|
|
619
626
|
*/
|
|
@@ -625,6 +632,10 @@ async function writeFileContent(filepath, content) {
|
|
|
625
632
|
await ensureDir(dirname(filepath));
|
|
626
633
|
await writeFile(filepath, content, "utf-8");
|
|
627
634
|
}
|
|
635
|
+
async function writeFileBuffer(filepath, buffer) {
|
|
636
|
+
await ensureDir(dirname(filepath));
|
|
637
|
+
await writeFile(filepath, buffer);
|
|
638
|
+
}
|
|
628
639
|
async function fileExists(filepath) {
|
|
629
640
|
try {
|
|
630
641
|
await stat(filepath);
|
|
@@ -1831,7 +1842,7 @@ var RulesyncFile = class extends AiFile {
|
|
|
1831
1842
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
1832
1843
|
* This excludes arrays and null values.
|
|
1833
1844
|
*/
|
|
1834
|
-
function isRecord(value) {
|
|
1845
|
+
function isRecord$1(value) {
|
|
1835
1846
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1836
1847
|
}
|
|
1837
1848
|
/**
|
|
@@ -1846,7 +1857,7 @@ function isRecord(value) {
|
|
|
1846
1857
|
* malicious accessor descriptors.
|
|
1847
1858
|
*/
|
|
1848
1859
|
function isPlainObject$1(value) {
|
|
1849
|
-
if (!isRecord(value)) return false;
|
|
1860
|
+
if (!isRecord$1(value)) return false;
|
|
1850
1861
|
const proto = Object.getPrototypeOf(value);
|
|
1851
1862
|
return proto === null || proto === Object.prototype;
|
|
1852
1863
|
}
|
|
@@ -2152,11 +2163,13 @@ const HookDefinitionSchema = z.looseObject({
|
|
|
2152
2163
|
timeout: z.optional(z.number()),
|
|
2153
2164
|
cacheTtl: z.optional(z.number().check(nonnegative())),
|
|
2154
2165
|
matcher: z.optional(safeString),
|
|
2166
|
+
enabled: z.optional(z.boolean()),
|
|
2155
2167
|
prompt: z.optional(safeString),
|
|
2156
2168
|
loop_limit: z.optional(z.nullable(z.number())),
|
|
2157
2169
|
name: z.optional(safeString),
|
|
2158
2170
|
description: z.optional(safeString),
|
|
2159
2171
|
failClosed: z.optional(z.boolean()),
|
|
2172
|
+
commandRegex: z.optional(safeString),
|
|
2160
2173
|
sequential: z.optional(z.boolean()),
|
|
2161
2174
|
async: z.optional(z.boolean()),
|
|
2162
2175
|
env: z.optional(z.record(z.string(), safeString)),
|
|
@@ -2173,6 +2186,7 @@ const HookDefinitionSchema = z.looseObject({
|
|
|
2173
2186
|
metadata: z.optional(z.looseObject({})),
|
|
2174
2187
|
if: z.optional(safeString),
|
|
2175
2188
|
commandWindows: z.optional(safeString),
|
|
2189
|
+
additionalContextLimit: z.optional(z.int().check(nonnegative())),
|
|
2176
2190
|
asyncRewake: z.optional(z.boolean()),
|
|
2177
2191
|
continueOnBlock: z.optional(z.boolean())
|
|
2178
2192
|
});
|
|
@@ -2468,8 +2482,9 @@ const FACTORYDROID_HOOK_EVENTS = [
|
|
|
2468
2482
|
/**
|
|
2469
2483
|
* Hook events supported by deepagents-cli (`deepagents-code` / `dcode`).
|
|
2470
2484
|
*
|
|
2471
|
-
*
|
|
2472
|
-
*
|
|
2485
|
+
* These are the twelve Hooks v2 `HookEvent` members, GA since deepagents-code
|
|
2486
|
+
* 0.1.52. Canonical `contextOffload` is deliberately absent — see
|
|
2487
|
+
* {@link CANONICAL_TO_DEEPAGENTS_EVENT_NAMES}.
|
|
2473
2488
|
* https://docs.langchain.com/oss/python/deepagents/cli/configuration
|
|
2474
2489
|
*/
|
|
2475
2490
|
const DEEPAGENTS_HOOK_EVENTS = [
|
|
@@ -2482,8 +2497,9 @@ const DEEPAGENTS_HOOK_EVENTS = [
|
|
|
2482
2497
|
"postToolUseFailure",
|
|
2483
2498
|
"stop",
|
|
2484
2499
|
"preCompact",
|
|
2485
|
-
"
|
|
2486
|
-
"
|
|
2500
|
+
"notification",
|
|
2501
|
+
"subagentStart",
|
|
2502
|
+
"subagentStop"
|
|
2487
2503
|
];
|
|
2488
2504
|
/** Hook events supported by Codex CLI. */
|
|
2489
2505
|
const CODEXCLI_HOOK_EVENTS = [
|
|
@@ -3171,26 +3187,59 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
|
|
|
3171
3187
|
*/
|
|
3172
3188
|
const GOOSE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GOOSE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
3173
3189
|
/**
|
|
3174
|
-
* Map canonical camelCase event names to deepagents-cli
|
|
3190
|
+
* Map canonical camelCase event names to the deepagents-cli Hooks v2
|
|
3191
|
+
* `HookEvent` values.
|
|
3192
|
+
*
|
|
3193
|
+
* Hooks v2 went GA in deepagents-code 0.1.52 (2026-08-04) and replaced the
|
|
3194
|
+
* legacy dot-notation names (`session.start`, `tool.use`, …) with these twelve
|
|
3195
|
+
* PascalCase members. The legacy list format is still read, but is scheduled for
|
|
3196
|
+
* removal on 2026-09-01 (`_LEGACY_HOOKS_REMOVAL_DATE` in `hooks/loading.py`).
|
|
3197
|
+
*
|
|
3198
|
+
* Canonical `contextOffload` has no v2 counterpart. Its legacy event
|
|
3199
|
+
* (`context.offload`) is gone, and folding it onto `PreCompact` would silently
|
|
3200
|
+
* merge two distinct canonical events into one — so it is dropped for
|
|
3201
|
+
* deepagents instead, and reported by the hooks processor as an unsupported
|
|
3202
|
+
* event like any other.
|
|
3203
|
+
*
|
|
3204
|
+
* @see https://github.com/langchain-ai/deepagents `libs/code/deepagents_code/hooks/models/domain.py`
|
|
3175
3205
|
*/
|
|
3176
3206
|
const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
|
|
3177
|
-
sessionStart: "
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
permissionRequest: "
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
preCompact: "
|
|
3186
|
-
|
|
3187
|
-
|
|
3207
|
+
sessionStart: "SessionStart",
|
|
3208
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
3209
|
+
sessionEnd: "SessionEnd",
|
|
3210
|
+
permissionRequest: "PermissionRequest",
|
|
3211
|
+
notification: "Notification",
|
|
3212
|
+
preToolUse: "PreToolUse",
|
|
3213
|
+
postToolUse: "PostToolUse",
|
|
3214
|
+
postToolUseFailure: "PostToolUseFailure",
|
|
3215
|
+
preCompact: "PreCompact",
|
|
3216
|
+
stop: "Stop",
|
|
3217
|
+
subagentStart: "SubagentStart",
|
|
3218
|
+
subagentStop: "SubagentStop"
|
|
3188
3219
|
};
|
|
3189
3220
|
/**
|
|
3190
|
-
* Map deepagents-cli
|
|
3221
|
+
* Map deepagents-cli `HookEvent` values to canonical camelCase.
|
|
3191
3222
|
*/
|
|
3192
3223
|
const DEEPAGENTS_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEEPAGENTS_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
3193
3224
|
/**
|
|
3225
|
+
* The legacy dot-notation event names deepagents-cli used before Hooks v2.
|
|
3226
|
+
* Kept for the read-only import path so a `hooks.json` still in the old format
|
|
3227
|
+
* round-trips into canonical events instead of being silently discarded.
|
|
3228
|
+
*/
|
|
3229
|
+
const DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES = {
|
|
3230
|
+
"session.start": "sessionStart",
|
|
3231
|
+
"session.end": "sessionEnd",
|
|
3232
|
+
"user.prompt": "beforeSubmitPrompt",
|
|
3233
|
+
"permission.request": "permissionRequest",
|
|
3234
|
+
"tool.use": "preToolUse",
|
|
3235
|
+
"tool.result": "postToolUse",
|
|
3236
|
+
"tool.error": "postToolUseFailure",
|
|
3237
|
+
"task.complete": "stop",
|
|
3238
|
+
"context.compact": "preCompact",
|
|
3239
|
+
"context.offload": "contextOffload",
|
|
3240
|
+
"input.required": "notification"
|
|
3241
|
+
};
|
|
3242
|
+
/**
|
|
3194
3243
|
* Map canonical camelCase event names to Kiro CLI camelCase.
|
|
3195
3244
|
* Kiro CLI uses its own event naming: agentSpawn, userPromptSubmit, preToolUse,
|
|
3196
3245
|
* postToolUse, stop. Both `sessionEnd` and `stop` canonical events map to
|
|
@@ -3808,7 +3857,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
3808
3857
|
logger
|
|
3809
3858
|
});
|
|
3810
3859
|
for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
|
|
3811
|
-
if (!isRecord(json[ignoredKey])) continue;
|
|
3860
|
+
if (!isRecord$1(json[ignoredKey])) continue;
|
|
3812
3861
|
this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
|
|
3813
3862
|
}
|
|
3814
3863
|
const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
|
|
@@ -3820,7 +3869,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
3820
3869
|
}));
|
|
3821
3870
|
for (const blockKey of blockKeys) {
|
|
3822
3871
|
const toolBlock = json[blockKey];
|
|
3823
|
-
const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
|
|
3872
|
+
const toolServers = isRecord$1(toolBlock) && isRecord$1(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
|
|
3824
3873
|
for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
|
|
3825
3874
|
if (isPrototypePollutionKey(serverName)) continue;
|
|
3826
3875
|
if (serverConfig === null) delete effectiveServers[serverName];
|
|
@@ -4220,7 +4269,8 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
4220
4269
|
*/
|
|
4221
4270
|
const FactorydroidPermissionsOverrideSchema = z.looseObject({
|
|
4222
4271
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
4223
|
-
commandBlocklist: z.optional(z.array(z.string()))
|
|
4272
|
+
commandBlocklist: z.optional(z.array(z.string())),
|
|
4273
|
+
disabledSkills: z.optional(z.array(z.string()))
|
|
4224
4274
|
});
|
|
4225
4275
|
/**
|
|
4226
4276
|
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
@@ -4666,13 +4716,22 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
|
|
|
4666
4716
|
* `base_permission_profile` it is consumed by the profile builder, not
|
|
4667
4717
|
* written as a top-level config key.
|
|
4668
4718
|
*
|
|
4669
|
-
*
|
|
4670
|
-
*
|
|
4671
|
-
*
|
|
4672
|
-
*
|
|
4673
|
-
*
|
|
4674
|
-
*
|
|
4675
|
-
*
|
|
4719
|
+
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
4720
|
+
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
4721
|
+
* (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
|
|
4722
|
+
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
|
|
4723
|
+
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
4724
|
+
* `base_permission_profile` and `git_write_rules` are consumed by the profile
|
|
4725
|
+
* builder rather than written, as described above, and `permission` is the
|
|
4726
|
+
* tool-scoped canonical block, which `RulesyncPermissions.forTarget` strips out
|
|
4727
|
+
* of the override before it ever reaches the patch. The allowlist is what keeps
|
|
4728
|
+
* the override from clobbering a feature-owned key: `mcp_servers.*` per-MCP
|
|
4729
|
+
* gating is owned by the MCP feature (`codexcli-mcp.ts` already writes the
|
|
4730
|
+
* `mcp_servers` tables in the same `config.toml`), and `permissions` /
|
|
4731
|
+
* `default_permissions` are owned by the canonical model. The schema itself is
|
|
4732
|
+
* `looseObject` so an unmodeled key parses (and is then reported rather than
|
|
4733
|
+
* rejected outright); supporting a new top-level Codex config key means adding
|
|
4734
|
+
* it to `CODEXCLI_OVERRIDE_KEYS`.
|
|
4676
4735
|
*
|
|
4677
4736
|
* @see https://developers.openai.com/codex/config-reference
|
|
4678
4737
|
* @see https://developers.openai.com/codex/permissions
|
|
@@ -4827,9 +4886,9 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
4827
4886
|
if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
|
|
4828
4887
|
const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
|
|
4829
4888
|
const json = this.json;
|
|
4830
|
-
if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
|
|
4889
|
+
if (overrideKey !== toolTarget && isRecord$1(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
|
|
4831
4890
|
const overrideBlock = json[overrideKey];
|
|
4832
|
-
if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
|
|
4891
|
+
if (!isRecord$1(overrideBlock) || !isRecord$1(overrideBlock.permission)) return this;
|
|
4833
4892
|
const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
|
|
4834
4893
|
const merged = {
|
|
4835
4894
|
...json,
|
|
@@ -5139,6 +5198,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5139
5198
|
compatibility: z.optional(z.looseObject({})),
|
|
5140
5199
|
metadata: z.optional(z.looseObject({}))
|
|
5141
5200
|
})),
|
|
5201
|
+
kiro: z.optional(z.looseObject({
|
|
5202
|
+
license: z.optional(z.string()),
|
|
5203
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
5204
|
+
metadata: z.optional(z.looseObject({}))
|
|
5205
|
+
})),
|
|
5142
5206
|
deepagents: z.optional(z.looseObject({
|
|
5143
5207
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
5144
5208
|
license: z.optional(z.string()),
|
|
@@ -5147,7 +5211,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5147
5211
|
})),
|
|
5148
5212
|
copilot: z.optional(z.looseObject({
|
|
5149
5213
|
license: z.optional(z.string()),
|
|
5150
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
5214
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
5215
|
+
"argument-hint": z.optional(z.string()),
|
|
5216
|
+
"user-invocable": z.optional(z.boolean()),
|
|
5217
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
5218
|
+
context: z.optional(z.string())
|
|
5151
5219
|
})),
|
|
5152
5220
|
copilotcli: z.optional(z.looseObject({
|
|
5153
5221
|
license: z.optional(z.string()),
|
|
@@ -5205,7 +5273,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5205
5273
|
})),
|
|
5206
5274
|
factorydroid: z.optional(z.looseObject({
|
|
5207
5275
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
5208
|
-
"user-invocable": z.optional(z.boolean())
|
|
5276
|
+
"user-invocable": z.optional(z.boolean()),
|
|
5277
|
+
enabled: z.optional(z.boolean()),
|
|
5278
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
5209
5279
|
})),
|
|
5210
5280
|
grokcli: z.optional(z.looseObject({
|
|
5211
5281
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
@@ -5418,8 +5488,8 @@ async function getLocalSkillDirNames(outputRoot) {
|
|
|
5418
5488
|
* Resolve the effective `disable-model-invocation` value for a tool skill.
|
|
5419
5489
|
*
|
|
5420
5490
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
5421
|
-
* default that applies to every tool supporting the flag (claudecode,
|
|
5422
|
-
* zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5491
|
+
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
5492
|
+
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5423
5493
|
* default with a per-target value. A defined section value (including `false`)
|
|
5424
5494
|
* always wins over the root default.
|
|
5425
5495
|
*
|
|
@@ -5432,8 +5502,8 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
5432
5502
|
* Resolve the effective `user-invocable` value for a tool skill.
|
|
5433
5503
|
*
|
|
5434
5504
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
5435
|
-
* that applies to every tool supporting the flag (claudecode,
|
|
5436
|
-
* grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5505
|
+
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
5506
|
+
* copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5437
5507
|
* per-target value. A defined section value (including `false`) always wins
|
|
5438
5508
|
* over the root default.
|
|
5439
5509
|
*
|
|
@@ -5646,6 +5716,31 @@ function fileContentsEquivalent({ filePath, expected, existing }) {
|
|
|
5646
5716
|
if (structured !== void 0) return structured;
|
|
5647
5717
|
return addTrailingNewline(expected) === addTrailingNewline(existing);
|
|
5648
5718
|
}
|
|
5719
|
+
/**
|
|
5720
|
+
* Whether an on-disk companion file is equivalent to the generated one.
|
|
5721
|
+
*
|
|
5722
|
+
* Companion files (everything beside a skill's `SKILL.md`) are written byte for
|
|
5723
|
+
* byte, so byte equality is the whole test for a user asset carried through
|
|
5724
|
+
* from the source directory: a CRLF fixture or a deliberately newline-less file
|
|
5725
|
+
* must compare equal to itself and unequal to a normalized copy, and a copy
|
|
5726
|
+
* that has drifted must be repaired rather than tolerated.
|
|
5727
|
+
*
|
|
5728
|
+
* A `composed` file is different — Rulesync builds it from frontmatter (Codex
|
|
5729
|
+
* CLI's `agents/openai.yaml`), so differing bytes fall back to the structured
|
|
5730
|
+
* comparison and a formatter re-indenting it is not reported as a change on
|
|
5731
|
+
* every generate. Only the structured verdict counts: there is deliberately no
|
|
5732
|
+
* text fallback, since trailing-whitespace-insensitive text equality is exactly
|
|
5733
|
+
* the normalization companion files no longer get.
|
|
5734
|
+
*/
|
|
5735
|
+
function companionFileContentsEquivalent({ filePath, expected, existing, composed = false }) {
|
|
5736
|
+
if (existing === null) return false;
|
|
5737
|
+
if (existing.equals(expected)) return true;
|
|
5738
|
+
if (!composed) return false;
|
|
5739
|
+
const expectedText = expected.toString("utf-8");
|
|
5740
|
+
const existingText = existing.toString("utf-8");
|
|
5741
|
+
if (!Buffer.from(expectedText, "utf-8").equals(expected) || !Buffer.from(existingText, "utf-8").equals(existing)) return false;
|
|
5742
|
+
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
5743
|
+
}
|
|
5649
5744
|
//#endregion
|
|
5650
5745
|
//#region src/types/feature-processor.ts
|
|
5651
5746
|
var FeatureProcessor = class {
|
|
@@ -7046,6 +7141,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7046
7141
|
ownedKeys: ["mcp", "tools"]
|
|
7047
7142
|
} }
|
|
7048
7143
|
},
|
|
7144
|
+
".config/goose/config.yaml": {
|
|
7145
|
+
format: "yaml",
|
|
7146
|
+
invalidRootPolicy: "error",
|
|
7147
|
+
features: { mcp: {
|
|
7148
|
+
kind: "replace-owned-keys",
|
|
7149
|
+
ownedKeys: ["extensions"]
|
|
7150
|
+
} }
|
|
7151
|
+
},
|
|
7049
7152
|
[CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
|
|
7050
7153
|
format: "toml",
|
|
7051
7154
|
features: {
|
|
@@ -8194,7 +8297,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
8194
8297
|
}
|
|
8195
8298
|
static extractAntigravityConfig(rulesyncCommand) {
|
|
8196
8299
|
const antigravity = rulesyncCommand.getFrontmatter().antigravity;
|
|
8197
|
-
return isRecord(antigravity) ? antigravity : void 0;
|
|
8300
|
+
return isRecord$1(antigravity) ? antigravity : void 0;
|
|
8198
8301
|
}
|
|
8199
8302
|
static resolveTrigger(rulesyncCommand, antigravityConfig) {
|
|
8200
8303
|
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
@@ -9284,9 +9387,10 @@ const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
|
9284
9387
|
//#region src/features/commands/factorydroid-command.ts
|
|
9285
9388
|
const FactorydroidCommandFrontmatterSchema = z.looseObject({
|
|
9286
9389
|
description: z.optional(z.string()),
|
|
9287
|
-
"argument-hint": z.optional(z.string())
|
|
9288
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
9390
|
+
"argument-hint": z.optional(z.string())
|
|
9289
9391
|
});
|
|
9392
|
+
/** Not a Droid command surface; see the schema comment above. */
|
|
9393
|
+
const FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS = ["allowed-tools"];
|
|
9290
9394
|
var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
9291
9395
|
frontmatter;
|
|
9292
9396
|
body;
|
|
@@ -9313,6 +9417,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9313
9417
|
}
|
|
9314
9418
|
toRulesyncCommand() {
|
|
9315
9419
|
const { description, ...restFields } = this.frontmatter;
|
|
9420
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete restFields[field];
|
|
9316
9421
|
const rulesyncFrontmatter = {
|
|
9317
9422
|
targets: ["*"],
|
|
9318
9423
|
description,
|
|
@@ -9336,6 +9441,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9336
9441
|
description: rulesyncFrontmatter.description,
|
|
9337
9442
|
...factorydroidFields
|
|
9338
9443
|
};
|
|
9444
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete factorydroidFrontmatter[field];
|
|
9339
9445
|
const body = rulesyncCommand.getBody();
|
|
9340
9446
|
const paths = this.getSettablePaths({ global });
|
|
9341
9447
|
return new FactorydroidCommand({
|
|
@@ -11981,8 +12087,8 @@ async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
|
|
|
11981
12087
|
}
|
|
11982
12088
|
if (!isPlainObject$1(parsed) || !Array.isArray(parsed.prompts)) return "";
|
|
11983
12089
|
const expectedContentFile = toPosixPath(join("prompts", relativeFilePath));
|
|
11984
|
-
const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
|
|
11985
|
-
return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
|
|
12090
|
+
const entry = parsed.prompts.find((candidate) => isRecord$1(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
|
|
12091
|
+
return isRecord$1(entry) && typeof entry.description === "string" ? entry.description : "";
|
|
11986
12092
|
}
|
|
11987
12093
|
/**
|
|
11988
12094
|
* The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
|
|
@@ -13047,16 +13153,28 @@ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
|
|
|
13047
13153
|
};
|
|
13048
13154
|
}
|
|
13049
13155
|
/**
|
|
13050
|
-
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13051
|
-
* preserving insertion order of both keys and grouped definitions.
|
|
13156
|
+
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13157
|
+
* absent), preserving insertion order of both keys and grouped definitions.
|
|
13158
|
+
* Definitions that disagree on a `subdividesGroup` passthrough field are split
|
|
13159
|
+
* into separate groups, so a restricting field is never inherited by a hook
|
|
13160
|
+
* that did not ask for it.
|
|
13052
13161
|
*/
|
|
13053
|
-
function groupDefinitionsByMatcher(definitions) {
|
|
13162
|
+
function groupDefinitionsByMatcher({ definitions, converterConfig }) {
|
|
13163
|
+
const subdividingFields = (converterConfig.groupPassthroughFields ?? []).filter(({ subdividesGroup }) => subdividesGroup);
|
|
13054
13164
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
13055
13165
|
for (const def of definitions) {
|
|
13056
|
-
const
|
|
13057
|
-
const
|
|
13058
|
-
|
|
13059
|
-
|
|
13166
|
+
const rawMatcher = def.matcher ?? "";
|
|
13167
|
+
const matcher = converterConfig.wildcardMatcherMeansAll && rawMatcher === "*" ? "" : rawMatcher;
|
|
13168
|
+
const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
|
|
13169
|
+
const value = def[canonical];
|
|
13170
|
+
return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
13171
|
+
})].join("\0");
|
|
13172
|
+
const group = byMatcher.get(key);
|
|
13173
|
+
if (group) group.defs.push(def);
|
|
13174
|
+
else byMatcher.set(key, {
|
|
13175
|
+
matcher,
|
|
13176
|
+
defs: [def]
|
|
13177
|
+
});
|
|
13060
13178
|
}
|
|
13061
13179
|
return byMatcher;
|
|
13062
13180
|
}
|
|
@@ -13083,59 +13201,148 @@ function applyCommandPrefix({ def, converterConfig }) {
|
|
|
13083
13201
|
return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
|
|
13084
13202
|
}
|
|
13085
13203
|
/**
|
|
13086
|
-
*
|
|
13087
|
-
*
|
|
13088
|
-
*
|
|
13204
|
+
* Whether a field registered for `command` hooks only applies to this hook.
|
|
13205
|
+
* Applied on both export and import: a value imported into a canonical field
|
|
13206
|
+
* the exporter would then drop is silently deleted on the next generate.
|
|
13089
13207
|
*/
|
|
13090
|
-
function
|
|
13091
|
-
return
|
|
13092
|
-
if (commandOnly === true && hookType !== "command") return false;
|
|
13093
|
-
return typeof def[canonical] === "boolean";
|
|
13094
|
-
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13208
|
+
function isFieldApplicable({ commandOnly, hookType }) {
|
|
13209
|
+
return commandOnly !== true || hookType === "command";
|
|
13095
13210
|
}
|
|
13096
13211
|
/**
|
|
13097
|
-
*
|
|
13098
|
-
*
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
*
|
|
13105
|
-
*
|
|
13106
|
-
*
|
|
13107
|
-
*/
|
|
13108
|
-
function emitStringPassthroughFields({ def, hookType, converterConfig }) {
|
|
13109
|
-
return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13110
|
-
if (commandOnly === true && hookType !== "command") return false;
|
|
13111
|
-
return typeof def[canonical] === "string" && def[canonical] !== "";
|
|
13112
|
-
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13113
|
-
}
|
|
13114
|
-
/**
|
|
13115
|
-
* Import the configured string passthrough fields back into canonical fields,
|
|
13116
|
-
* reversing {@link emitStringPassthroughFields}. Only non-empty string values
|
|
13117
|
-
* are read.
|
|
13212
|
+
* Emit the configured passthrough fields on the tool side, mapping each
|
|
13213
|
+
* canonical field name to its (possibly renamed) tool field name. Only values
|
|
13214
|
+
* accepted by `isValid` are carried through, so a malformed field can't leak
|
|
13215
|
+
* into a config the tool would reject.
|
|
13216
|
+
*
|
|
13217
|
+
* A field the tool documents on `command` hooks only is dropped when authored
|
|
13218
|
+
* on another hook type. That is a value the user hand-wrote in
|
|
13219
|
+
* `.rulesync/hooks.*` and the canonical schema does not cross-validate it
|
|
13220
|
+
* against the hook's type, so it is warned about rather than deleted in
|
|
13221
|
+
* silence — the mirror of the import side.
|
|
13118
13222
|
*/
|
|
13119
|
-
function
|
|
13120
|
-
|
|
13121
|
-
|
|
13223
|
+
function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn }) {
|
|
13224
|
+
for (const { canonical, tool, commandOnly } of fields) {
|
|
13225
|
+
const value = def[canonical];
|
|
13226
|
+
if (value === void 0) continue;
|
|
13227
|
+
if (!isFieldApplicable({
|
|
13228
|
+
commandOnly,
|
|
13229
|
+
hookType
|
|
13230
|
+
})) {
|
|
13231
|
+
warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not generated.`);
|
|
13232
|
+
continue;
|
|
13233
|
+
}
|
|
13234
|
+
if (!isValid({
|
|
13235
|
+
value,
|
|
13236
|
+
canonical
|
|
13237
|
+
})) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${JSON.stringify(value)} is not a value this tool can express as "${tool}".`);
|
|
13238
|
+
}
|
|
13239
|
+
return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
|
|
13240
|
+
commandOnly,
|
|
13241
|
+
hookType
|
|
13242
|
+
}) && isValid({
|
|
13243
|
+
value: def[canonical],
|
|
13244
|
+
canonical
|
|
13245
|
+
})).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13246
|
+
}
|
|
13247
|
+
/**
|
|
13248
|
+
* Import the configured passthrough fields back into canonical fields,
|
|
13249
|
+
* reversing {@link emitPassthroughFields}. A field the tool documents on
|
|
13250
|
+
* `command` hooks only is skipped here too, and `describeInvalid` — when the
|
|
13251
|
+
* kind has a rule an authored file can plausibly violate — turns a rejected
|
|
13252
|
+
* value into a warning instead of a silent drop.
|
|
13253
|
+
*/
|
|
13254
|
+
function importPassthroughFields({ h, hookType, fields, isValid, describeInvalid, warn }) {
|
|
13255
|
+
const applicable = fields.filter(({ commandOnly }) => isFieldApplicable({
|
|
13256
|
+
commandOnly,
|
|
13257
|
+
hookType
|
|
13258
|
+
}));
|
|
13259
|
+
const skipped = fields.filter(({ commandOnly }) => !isFieldApplicable({
|
|
13260
|
+
commandOnly,
|
|
13261
|
+
hookType
|
|
13262
|
+
}));
|
|
13263
|
+
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.`);
|
|
13264
|
+
for (const { tool, canonical } of applicable) if (describeInvalid !== void 0 && h[tool] !== void 0 && !isValid({
|
|
13265
|
+
value: h[tool],
|
|
13266
|
+
canonical
|
|
13267
|
+
})) warn?.(describeInvalid({
|
|
13268
|
+
tool,
|
|
13269
|
+
canonical,
|
|
13270
|
+
value: h[tool]
|
|
13271
|
+
}));
|
|
13272
|
+
return Object.fromEntries(applicable.filter(({ tool, canonical }) => isValid({
|
|
13273
|
+
value: h[tool],
|
|
13274
|
+
canonical
|
|
13275
|
+
})).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13276
|
+
}
|
|
13277
|
+
const isBooleanValue = ({ value }) => typeof value === "boolean";
|
|
13278
|
+
const isFiniteNumber = ({ value }) => Number.isFinite(value);
|
|
13279
|
+
const isNonEmptyString = (value) => typeof value === "string" && value !== "";
|
|
13280
|
+
const isEmittableString = ({ value }) => isNonEmptyString(value);
|
|
13281
|
+
const isEmittableArray = ({ value }) => isStringArray(value);
|
|
13282
|
+
const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
|
|
13283
|
+
const isImportableArray = ({ value }) => isSafeStringArray(value);
|
|
13284
|
+
/**
|
|
13285
|
+
* The canonical schema of one hook field, looked up by name. Read off the
|
|
13286
|
+
* schema's own shape rather than by parsing a one-field object, so a `canonical`
|
|
13287
|
+
* name that no longer exists resolves to `undefined` (a `looseObject` would
|
|
13288
|
+
* accept an unknown key and silently validate nothing) and a typo is caught by
|
|
13289
|
+
* the tests instead of quietly disabling the check.
|
|
13290
|
+
*/
|
|
13291
|
+
const CANONICAL_FIELD_SCHEMAS = HookDefinitionSchema.def.shape;
|
|
13292
|
+
/**
|
|
13293
|
+
* Whether a value satisfies the constraints the canonical schema puts on the
|
|
13294
|
+
* field it would be imported into.
|
|
13295
|
+
*
|
|
13296
|
+
* Import needs this on top of the kind's shape check: a hand-written tool
|
|
13297
|
+
* settings file can hold `"shell": "zsh"`, which is a non-empty string but not
|
|
13298
|
+
* a member of the canonical `z.enum(["bash", "powershell"])`. Letting it in
|
|
13299
|
+
* would write a `.rulesync/hooks.jsonc` that fails validation on the *next*
|
|
13300
|
+
* run, taking the whole hooks feature down with it.
|
|
13301
|
+
*/
|
|
13302
|
+
function satisfiesCanonicalField({ value, canonical }) {
|
|
13303
|
+
const schema = canonicalFieldSchema(canonical);
|
|
13304
|
+
return schema !== void 0 && z.safeParse(schema, value).success;
|
|
13305
|
+
}
|
|
13306
|
+
/** Own properties only, so a name like `toString` resolves to nothing. */
|
|
13307
|
+
function canonicalFieldSchema(canonical) {
|
|
13308
|
+
return Object.hasOwn(CANONICAL_FIELD_SCHEMAS, canonical) ? CANONICAL_FIELD_SCHEMAS[canonical] : void 0;
|
|
13309
|
+
}
|
|
13310
|
+
const isImportableString = ({ value, canonical }) => isNonEmptyString(value) && satisfiesCanonicalField({
|
|
13311
|
+
value,
|
|
13312
|
+
canonical
|
|
13313
|
+
});
|
|
13314
|
+
const isImportableNumber = ({ value, canonical }) => Number.isFinite(value) && satisfiesCanonicalField({
|
|
13315
|
+
value,
|
|
13316
|
+
canonical
|
|
13317
|
+
});
|
|
13122
13318
|
/**
|
|
13123
|
-
*
|
|
13319
|
+
* Say which rule the value broke, so the warning names the actual constraint
|
|
13320
|
+
* rather than asserting a canonical rejection that may not be the reason. A
|
|
13321
|
+
* closed enum lists its members; a rule carrying its own message (the
|
|
13322
|
+
* control-character check behind `safeString`) reuses it.
|
|
13124
13323
|
*/
|
|
13125
|
-
function
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13324
|
+
function describeScalarConstraint({ canonical, value }) {
|
|
13325
|
+
const schema = canonicalFieldSchema(canonical);
|
|
13326
|
+
const result = schema === void 0 ? void 0 : z.safeParse(schema, value);
|
|
13327
|
+
if (result === void 0 || result.success) return `it is not a value this field carries through.`;
|
|
13328
|
+
const issue = result.error.issues[0];
|
|
13329
|
+
if (issue === void 0) return `it is not a value the canonical "${canonical}" field accepts.`;
|
|
13330
|
+
return `it does not satisfy the canonical "${canonical}" field: ${issue.message}.`;
|
|
13130
13331
|
}
|
|
13332
|
+
const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${JSON.stringify(value)}) while importing a hook: ${describeScalarConstraint({
|
|
13333
|
+
canonical,
|
|
13334
|
+
value
|
|
13335
|
+
})} Importing it would fail validation on the next run.`;
|
|
13336
|
+
const describeInvalidArray = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a list of strings without newline, carriage return or NUL characters.`;
|
|
13337
|
+
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.`;
|
|
13131
13338
|
/**
|
|
13132
|
-
*
|
|
13133
|
-
*
|
|
13339
|
+
* Check a value against the shape its field documents. A string field also
|
|
13340
|
+
* rejects control characters, matching the canonical `safeString` so an
|
|
13341
|
+
* imported value cannot fail validation on the next generate.
|
|
13134
13342
|
*/
|
|
13135
|
-
function
|
|
13136
|
-
|
|
13137
|
-
|
|
13138
|
-
return Object.fromEntries(fields.filter(({ tool }) => isSafeStringArray(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13343
|
+
function isGroupPassthroughValue(value, valueType = "object") {
|
|
13344
|
+
if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
|
|
13345
|
+
return isPlainObject$1(value);
|
|
13139
13346
|
}
|
|
13140
13347
|
/**
|
|
13141
13348
|
* Emit the configured group-level passthrough fields, taken from the first
|
|
@@ -13143,12 +13350,12 @@ function importArrayPassthroughFields({ h, converterConfig, logger }) {
|
|
|
13143
13350
|
*/
|
|
13144
13351
|
function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
|
|
13145
13352
|
const emitted = {};
|
|
13146
|
-
for (const { canonical, tool } of converterConfig.groupPassthroughFields ?? []) {
|
|
13353
|
+
for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
|
|
13147
13354
|
const carried = defs.map((def) => def[canonical]);
|
|
13148
|
-
const first = carried.find((value) =>
|
|
13355
|
+
const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
|
|
13149
13356
|
if (first === void 0) continue;
|
|
13150
13357
|
const firstStable = stableJson(first);
|
|
13151
|
-
const agrees = (value) =>
|
|
13358
|
+
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
13152
13359
|
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.`);
|
|
13153
13360
|
emitted[tool] = first;
|
|
13154
13361
|
}
|
|
@@ -13160,7 +13367,7 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
13160
13367
|
*/
|
|
13161
13368
|
function importGroupPassthroughFields({ rawEntry, converterConfig }) {
|
|
13162
13369
|
const entry = rawEntry;
|
|
13163
|
-
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool }) =>
|
|
13370
|
+
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
13164
13371
|
}
|
|
13165
13372
|
/**
|
|
13166
13373
|
* Emit the payload fields specific to a hook type — `url`/`headers`/
|
|
@@ -13188,7 +13395,54 @@ function emitTypePayloadFields({ def, hookType, converterConfig }) {
|
|
|
13188
13395
|
function isSupportedHookType({ type, converterConfig }) {
|
|
13189
13396
|
return converterConfig.supportedHookTypes?.has(type ?? "command") ?? true;
|
|
13190
13397
|
}
|
|
13191
|
-
|
|
13398
|
+
/**
|
|
13399
|
+
* Emit every per-hook passthrough kind for one canonical definition.
|
|
13400
|
+
*/
|
|
13401
|
+
function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, warn }) {
|
|
13402
|
+
return {
|
|
13403
|
+
...emitPassthroughFields({
|
|
13404
|
+
def,
|
|
13405
|
+
hookType,
|
|
13406
|
+
eventName,
|
|
13407
|
+
fields: converterConfig.booleanPassthroughFields ?? [],
|
|
13408
|
+
isValid: isBooleanValue,
|
|
13409
|
+
warn
|
|
13410
|
+
}),
|
|
13411
|
+
...emitPassthroughFields({
|
|
13412
|
+
def,
|
|
13413
|
+
hookType,
|
|
13414
|
+
eventName,
|
|
13415
|
+
fields: converterConfig.numberPassthroughFields ?? [],
|
|
13416
|
+
isValid: isFiniteNumber,
|
|
13417
|
+
warn
|
|
13418
|
+
}),
|
|
13419
|
+
...emitPassthroughFields({
|
|
13420
|
+
def,
|
|
13421
|
+
hookType,
|
|
13422
|
+
eventName,
|
|
13423
|
+
fields: converterConfig.stringPassthroughFields ?? [],
|
|
13424
|
+
isValid: isEmittableString,
|
|
13425
|
+
warn
|
|
13426
|
+
}),
|
|
13427
|
+
...emitPassthroughFields({
|
|
13428
|
+
def,
|
|
13429
|
+
hookType,
|
|
13430
|
+
eventName,
|
|
13431
|
+
fields: converterConfig.arrayPassthroughFields ?? [],
|
|
13432
|
+
isValid: isEmittableArray,
|
|
13433
|
+
warn
|
|
13434
|
+
}),
|
|
13435
|
+
...emitPassthroughFields({
|
|
13436
|
+
def,
|
|
13437
|
+
hookType,
|
|
13438
|
+
eventName,
|
|
13439
|
+
fields: converterConfig.recordPassthroughFields ?? [],
|
|
13440
|
+
isValid: isEmittableRecord,
|
|
13441
|
+
warn
|
|
13442
|
+
})
|
|
13443
|
+
};
|
|
13444
|
+
}
|
|
13445
|
+
function buildToolHooks({ defs, eventName, converterConfig, warn }) {
|
|
13192
13446
|
const hooks = [];
|
|
13193
13447
|
for (const def of defs) {
|
|
13194
13448
|
const hookType = def.type ?? "command";
|
|
@@ -13201,20 +13455,12 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13201
13455
|
converterConfig
|
|
13202
13456
|
});
|
|
13203
13457
|
hooks.push({
|
|
13204
|
-
...
|
|
13458
|
+
...emitAllPassthroughFields({
|
|
13205
13459
|
def,
|
|
13206
13460
|
hookType,
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
def,
|
|
13211
|
-
hookType,
|
|
13212
|
-
converterConfig
|
|
13213
|
-
}),
|
|
13214
|
-
...emitArrayPassthroughFields({
|
|
13215
|
-
def,
|
|
13216
|
-
hookType,
|
|
13217
|
-
converterConfig
|
|
13461
|
+
eventName,
|
|
13462
|
+
converterConfig,
|
|
13463
|
+
warn
|
|
13218
13464
|
}),
|
|
13219
13465
|
type: hookType,
|
|
13220
13466
|
...command !== void 0 && command !== null && { command },
|
|
@@ -13232,6 +13478,17 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13232
13478
|
return hooks;
|
|
13233
13479
|
}
|
|
13234
13480
|
/**
|
|
13481
|
+
* A `warn` that says each distinct thing once per conversion.
|
|
13482
|
+
*/
|
|
13483
|
+
function warnOnce(logger) {
|
|
13484
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13485
|
+
return (message) => {
|
|
13486
|
+
if (seen.has(message)) return;
|
|
13487
|
+
seen.add(message);
|
|
13488
|
+
logger?.warn(message);
|
|
13489
|
+
};
|
|
13490
|
+
}
|
|
13491
|
+
/**
|
|
13235
13492
|
* Convert canonical hooks config to tool-specific format (shared by Claude and Factory Droid).
|
|
13236
13493
|
* Uses explicit event name mapping tables rather than algorithmic case conversion,
|
|
13237
13494
|
* since tool event names may differ entirely from canonical names
|
|
@@ -13243,17 +13500,23 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
13243
13500
|
toolOverrideHooks,
|
|
13244
13501
|
supportedEvents: converterConfig.supportedEvents
|
|
13245
13502
|
});
|
|
13503
|
+
const warn = warnOnce(logger);
|
|
13246
13504
|
const result = {};
|
|
13247
13505
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
13248
13506
|
const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
|
|
13249
|
-
const byMatcher = groupDefinitionsByMatcher(
|
|
13507
|
+
const byMatcher = groupDefinitionsByMatcher({
|
|
13508
|
+
definitions,
|
|
13509
|
+
converterConfig
|
|
13510
|
+
});
|
|
13250
13511
|
const entries = [];
|
|
13251
13512
|
const isNoMatcherEvent = converterConfig.noMatcherEvents?.has(eventName) ?? false;
|
|
13252
|
-
for (const
|
|
13513
|
+
for (const { matcher: matcherKey, defs } of byMatcher.values()) {
|
|
13253
13514
|
if (isNoMatcherEvent && matcherKey) logger?.warn(`matcher "${matcherKey}" on "${eventName}" hook will be ignored — this event does not support matchers`);
|
|
13254
13515
|
const hooks = buildToolHooks({
|
|
13255
13516
|
defs,
|
|
13256
|
-
|
|
13517
|
+
eventName,
|
|
13518
|
+
converterConfig,
|
|
13519
|
+
warn
|
|
13257
13520
|
});
|
|
13258
13521
|
if (hooks.length === 0) continue;
|
|
13259
13522
|
const includeMatcher = matcherKey && !isNoMatcherEvent;
|
|
@@ -13329,9 +13592,27 @@ function isStringArray(value) {
|
|
|
13329
13592
|
}
|
|
13330
13593
|
/** Compare object values without letting key order decide the answer. */
|
|
13331
13594
|
function stableJson(value) {
|
|
13595
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
13332
13596
|
return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
|
|
13333
13597
|
}
|
|
13334
13598
|
/**
|
|
13599
|
+
* A string map safe to hand a tool as a hook's environment block. On top of
|
|
13600
|
+
* {@link isStringRecord} it rejects a non-plain object (a class instance is not
|
|
13601
|
+
* data) and applies the control-character rule to the values, as
|
|
13602
|
+
* {@link isSafeStringArray} does for `args`.
|
|
13603
|
+
*
|
|
13604
|
+
* The keys are checked more strictly than the values. A tool builds each entry
|
|
13605
|
+
* back into a `KEY=VALUE` string for the spawned process, so a key holding `=`
|
|
13606
|
+
* (or a control character, or nothing at all) names a different variable than
|
|
13607
|
+
* it appears to — `PATH=/tmp/evil` written as a key would set `PATH`. An
|
|
13608
|
+
* authored `.rulesync/hooks.*` can arrive via `rulesync fetch`, so that is not
|
|
13609
|
+
* a shape to pass along.
|
|
13610
|
+
*/
|
|
13611
|
+
function isSafeStringRecord(value) {
|
|
13612
|
+
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
13613
|
+
return Object.entries(value).every(([key, entry]) => key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || entry.includes(char)));
|
|
13614
|
+
}
|
|
13615
|
+
/**
|
|
13335
13616
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
13336
13617
|
* field the schema guards with `safeString`, or the next generate fails
|
|
13337
13618
|
* validation on a file this import itself wrote — and the hooks feature is
|
|
@@ -13341,34 +13622,152 @@ function isSafeStringArray(value) {
|
|
|
13341
13622
|
return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
|
|
13342
13623
|
}
|
|
13343
13624
|
/**
|
|
13625
|
+
* A raw string kept only if the canonical field it would land in accepts it,
|
|
13626
|
+
* warning when it does not. The canonical string fields are guarded by
|
|
13627
|
+
* `safeString`, so an existing tool config carrying a control character would
|
|
13628
|
+
* otherwise be imported into a file the next generate refuses to read.
|
|
13629
|
+
*/
|
|
13630
|
+
function importCanonicalString({ value, canonical, warn }) {
|
|
13631
|
+
if (typeof value !== "string") return;
|
|
13632
|
+
if (satisfiesCanonicalField({
|
|
13633
|
+
value,
|
|
13634
|
+
canonical
|
|
13635
|
+
})) return value;
|
|
13636
|
+
warn?.(describeInvalidScalar({
|
|
13637
|
+
tool: canonical,
|
|
13638
|
+
canonical,
|
|
13639
|
+
value
|
|
13640
|
+
}));
|
|
13641
|
+
}
|
|
13642
|
+
/**
|
|
13344
13643
|
* Import the payload fields specific to a hook type, type-checking each raw
|
|
13345
13644
|
* value before it enters the canonical definition.
|
|
13346
13645
|
*/
|
|
13347
|
-
function importTypePayloadFields({ h, hookType }) {
|
|
13348
|
-
if (hookType === "http")
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
13646
|
+
function importTypePayloadFields({ h, hookType, warn }) {
|
|
13647
|
+
if (hookType === "http") {
|
|
13648
|
+
const url = importCanonicalString({
|
|
13649
|
+
value: h.url,
|
|
13650
|
+
canonical: "url",
|
|
13651
|
+
warn
|
|
13652
|
+
});
|
|
13653
|
+
const headers = isStringRecord(h.headers) && !satisfiesCanonicalField({
|
|
13654
|
+
value: h.headers,
|
|
13655
|
+
canonical: "headers"
|
|
13656
|
+
}) ? (warn?.(describeInvalidScalar({
|
|
13657
|
+
tool: "headers",
|
|
13658
|
+
canonical: "headers",
|
|
13659
|
+
value: h.headers
|
|
13660
|
+
})), void 0) : h.headers;
|
|
13661
|
+
return {
|
|
13662
|
+
...url !== void 0 && { url },
|
|
13663
|
+
...isStringRecord(headers) && { headers },
|
|
13664
|
+
...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
|
|
13665
|
+
};
|
|
13666
|
+
}
|
|
13667
|
+
if (hookType === "mcp_tool") {
|
|
13668
|
+
const server = importCanonicalString({
|
|
13669
|
+
value: h.server,
|
|
13670
|
+
canonical: "server",
|
|
13671
|
+
warn
|
|
13672
|
+
});
|
|
13673
|
+
const tool = importCanonicalString({
|
|
13674
|
+
value: h.tool,
|
|
13675
|
+
canonical: "tool",
|
|
13676
|
+
warn
|
|
13677
|
+
});
|
|
13678
|
+
return {
|
|
13679
|
+
...server !== void 0 && { server },
|
|
13680
|
+
...tool !== void 0 && { tool },
|
|
13681
|
+
...h.input !== null && typeof h.input === "object" && !Array.isArray(h.input) && { input: h.input }
|
|
13682
|
+
};
|
|
13683
|
+
}
|
|
13684
|
+
if (hookType === "prompt" || hookType === "agent") {
|
|
13685
|
+
const model = importCanonicalString({
|
|
13686
|
+
value: h.model,
|
|
13687
|
+
canonical: "model",
|
|
13688
|
+
warn
|
|
13689
|
+
});
|
|
13690
|
+
return model !== void 0 ? { model } : {};
|
|
13691
|
+
}
|
|
13359
13692
|
return {};
|
|
13360
13693
|
}
|
|
13361
13694
|
/**
|
|
13695
|
+
* Import every per-hook passthrough kind for one tool hook record, reversing
|
|
13696
|
+
* the emit side in {@link buildToolHooks}.
|
|
13697
|
+
*/
|
|
13698
|
+
function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
|
|
13699
|
+
return {
|
|
13700
|
+
...importPassthroughFields({
|
|
13701
|
+
h,
|
|
13702
|
+
hookType,
|
|
13703
|
+
fields: converterConfig.booleanPassthroughFields ?? [],
|
|
13704
|
+
isValid: isBooleanValue,
|
|
13705
|
+
warn
|
|
13706
|
+
}),
|
|
13707
|
+
...importPassthroughFields({
|
|
13708
|
+
h,
|
|
13709
|
+
hookType,
|
|
13710
|
+
fields: converterConfig.numberPassthroughFields ?? [],
|
|
13711
|
+
isValid: isImportableNumber,
|
|
13712
|
+
describeInvalid: describeInvalidScalar,
|
|
13713
|
+
warn
|
|
13714
|
+
}),
|
|
13715
|
+
...importPassthroughFields({
|
|
13716
|
+
h,
|
|
13717
|
+
hookType,
|
|
13718
|
+
fields: converterConfig.stringPassthroughFields ?? [],
|
|
13719
|
+
isValid: isImportableString,
|
|
13720
|
+
describeInvalid: describeInvalidScalar,
|
|
13721
|
+
warn
|
|
13722
|
+
}),
|
|
13723
|
+
...importPassthroughFields({
|
|
13724
|
+
h,
|
|
13725
|
+
hookType,
|
|
13726
|
+
fields: converterConfig.arrayPassthroughFields ?? [],
|
|
13727
|
+
isValid: isImportableArray,
|
|
13728
|
+
describeInvalid: describeInvalidArray,
|
|
13729
|
+
warn
|
|
13730
|
+
}),
|
|
13731
|
+
...importPassthroughFields({
|
|
13732
|
+
h,
|
|
13733
|
+
hookType,
|
|
13734
|
+
fields: converterConfig.recordPassthroughFields ?? [],
|
|
13735
|
+
isValid: isEmittableRecord,
|
|
13736
|
+
describeInvalid: describeInvalidRecord,
|
|
13737
|
+
warn
|
|
13738
|
+
})
|
|
13739
|
+
};
|
|
13740
|
+
}
|
|
13741
|
+
/**
|
|
13362
13742
|
* Convert a single tool hook record into a canonical hook definition.
|
|
13363
13743
|
*/
|
|
13364
|
-
function toolHookToCanonical({ h, rawEntry, converterConfig,
|
|
13365
|
-
const command = stripCommandPrefix({
|
|
13366
|
-
command: h.command,
|
|
13367
|
-
converterConfig
|
|
13368
|
-
});
|
|
13744
|
+
function toolHookToCanonical({ h, rawEntry, converterConfig, warn }) {
|
|
13369
13745
|
const hookType = isImportedHookType(h.type) ? h.type : "command";
|
|
13746
|
+
const command = importCanonicalString({
|
|
13747
|
+
value: stripCommandPrefix({
|
|
13748
|
+
command: h.command,
|
|
13749
|
+
converterConfig
|
|
13750
|
+
}),
|
|
13751
|
+
canonical: "command",
|
|
13752
|
+
warn
|
|
13753
|
+
});
|
|
13370
13754
|
const timeout = typeof h.timeout === "number" ? h.timeout : void 0;
|
|
13371
|
-
const prompt =
|
|
13755
|
+
const prompt = importCanonicalString({
|
|
13756
|
+
value: h.prompt,
|
|
13757
|
+
canonical: "prompt",
|
|
13758
|
+
warn
|
|
13759
|
+
});
|
|
13760
|
+
const name = importCanonicalString({
|
|
13761
|
+
value: h.name,
|
|
13762
|
+
canonical: "name",
|
|
13763
|
+
warn
|
|
13764
|
+
});
|
|
13765
|
+
const description = importCanonicalString({
|
|
13766
|
+
value: h.description,
|
|
13767
|
+
canonical: "description",
|
|
13768
|
+
warn
|
|
13769
|
+
});
|
|
13770
|
+
const matcher = rawEntry.matcher;
|
|
13372
13771
|
return {
|
|
13373
13772
|
type: hookType,
|
|
13374
13773
|
...command !== void 0 && command !== null && { command },
|
|
@@ -13376,40 +13775,130 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
|
|
|
13376
13775
|
...prompt !== void 0 && prompt !== null && { prompt },
|
|
13377
13776
|
...importTypePayloadFields({
|
|
13378
13777
|
h,
|
|
13379
|
-
hookType
|
|
13380
|
-
|
|
13381
|
-
...converterConfig.passthroughFields?.includes("name") && typeof h.name === "string" && { name: h.name },
|
|
13382
|
-
...converterConfig.passthroughFields?.includes("description") && typeof h.description === "string" && { description: h.description },
|
|
13383
|
-
...importBooleanPassthroughFields({
|
|
13384
|
-
h,
|
|
13385
|
-
converterConfig
|
|
13778
|
+
hookType,
|
|
13779
|
+
warn
|
|
13386
13780
|
}),
|
|
13387
|
-
...
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
}),
|
|
13391
|
-
...importArrayPassthroughFields({
|
|
13781
|
+
...converterConfig.passthroughFields?.includes("name") && name !== void 0 && { name },
|
|
13782
|
+
...converterConfig.passthroughFields?.includes("description") && description !== void 0 && { description },
|
|
13783
|
+
...importAllPassthroughFields({
|
|
13392
13784
|
h,
|
|
13785
|
+
hookType,
|
|
13393
13786
|
converterConfig,
|
|
13394
|
-
|
|
13787
|
+
warn
|
|
13395
13788
|
}),
|
|
13396
13789
|
...importGroupPassthroughFields({
|
|
13397
13790
|
rawEntry,
|
|
13398
13791
|
converterConfig
|
|
13399
13792
|
}),
|
|
13400
|
-
...
|
|
13793
|
+
...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
|
|
13401
13794
|
};
|
|
13402
13795
|
}
|
|
13403
13796
|
/**
|
|
13404
|
-
*
|
|
13797
|
+
* The fields whose value decides what a hook *is*, listed per hook type. When
|
|
13798
|
+
* one of them cannot be imported, dropping just the field would leave
|
|
13799
|
+
* something worse than nothing: a hook that loses its body runs nothing, and
|
|
13800
|
+
* one that loses its `matcher` fires on *everything* — a silent widening of
|
|
13801
|
+
* what the imported rule does. So the whole definition is skipped instead,
|
|
13802
|
+
* with the reason named. A field that does not define *this* type (a `prompt`
|
|
13803
|
+
* left on a command hook) is not one of them: it is dropped on its own, the
|
|
13804
|
+
* way any other unusable field is.
|
|
13805
|
+
*/
|
|
13806
|
+
function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
13807
|
+
const fields = [{
|
|
13808
|
+
field: "matcher",
|
|
13809
|
+
value: rawEntry.matcher
|
|
13810
|
+
}];
|
|
13811
|
+
if (hookType === "command") fields.push({
|
|
13812
|
+
field: "command",
|
|
13813
|
+
value: typeof h.command === "string" ? stripCommandPrefix({
|
|
13814
|
+
command: h.command,
|
|
13815
|
+
converterConfig
|
|
13816
|
+
}) : h.command
|
|
13817
|
+
});
|
|
13818
|
+
if (hookType === "prompt" || hookType === "agent") fields.push({
|
|
13819
|
+
field: "prompt",
|
|
13820
|
+
value: h.prompt
|
|
13821
|
+
});
|
|
13822
|
+
if (hookType === "http") fields.push({
|
|
13823
|
+
field: "url",
|
|
13824
|
+
value: h.url
|
|
13825
|
+
});
|
|
13826
|
+
if (hookType === "mcp_tool") fields.push({
|
|
13827
|
+
field: "server",
|
|
13828
|
+
value: h.server
|
|
13829
|
+
}, {
|
|
13830
|
+
field: "tool",
|
|
13831
|
+
value: h.tool
|
|
13832
|
+
});
|
|
13833
|
+
return fields;
|
|
13834
|
+
}
|
|
13835
|
+
/**
|
|
13836
|
+
* Why no hook of this matcher group can be imported, or `undefined` when they
|
|
13837
|
+
* can. A group field that *restricts* when its hooks run (`subdividesGroup`)
|
|
13838
|
+
* is the group-level twin of `matcher`: importing the group without it would
|
|
13839
|
+
* widen every hook in it, so the group is skipped instead.
|
|
13405
13840
|
*/
|
|
13406
|
-
function
|
|
13407
|
-
|
|
13841
|
+
function describeGroupSkipReason({ rawEntry, converterConfig }) {
|
|
13842
|
+
const entry = rawEntry;
|
|
13843
|
+
for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
|
|
13844
|
+
const value = entry[tool];
|
|
13845
|
+
if (subdividesGroup !== true || value === void 0) continue;
|
|
13846
|
+
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.`;
|
|
13847
|
+
}
|
|
13848
|
+
}
|
|
13849
|
+
/**
|
|
13850
|
+
* Why this hook cannot be imported, or `undefined` when it can.
|
|
13851
|
+
*/
|
|
13852
|
+
function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
|
|
13853
|
+
for (const { field, value } of definingFields({
|
|
13408
13854
|
h,
|
|
13409
13855
|
rawEntry,
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
}))
|
|
13856
|
+
hookType,
|
|
13857
|
+
converterConfig
|
|
13858
|
+
})) {
|
|
13859
|
+
if (value === void 0 || satisfiesCanonicalField({
|
|
13860
|
+
value,
|
|
13861
|
+
canonical: field
|
|
13862
|
+
})) continue;
|
|
13863
|
+
return `Skipping a hook while importing: its "${field}" (${JSON.stringify(value)}) is unusable — ${describeScalarConstraint({
|
|
13864
|
+
canonical: field,
|
|
13865
|
+
value
|
|
13866
|
+
})} Keeping the hook without it would change what it does, so the whole hook is skipped.`;
|
|
13867
|
+
}
|
|
13868
|
+
}
|
|
13869
|
+
/**
|
|
13870
|
+
* Convert a single tool matcher entry into canonical hook definitions.
|
|
13871
|
+
*/
|
|
13872
|
+
function toolMatcherEntryToCanonical({ rawEntry, converterConfig, warn }) {
|
|
13873
|
+
const hookDefs = rawEntry.hooks ?? [];
|
|
13874
|
+
const groupSkipReason = describeGroupSkipReason({
|
|
13875
|
+
rawEntry,
|
|
13876
|
+
converterConfig
|
|
13877
|
+
});
|
|
13878
|
+
if (groupSkipReason !== void 0) {
|
|
13879
|
+
warn?.(groupSkipReason);
|
|
13880
|
+
return [];
|
|
13881
|
+
}
|
|
13882
|
+
const definitions = [];
|
|
13883
|
+
for (const h of hookDefs) {
|
|
13884
|
+
const skipReason = describeHookSkipReason({
|
|
13885
|
+
h,
|
|
13886
|
+
rawEntry,
|
|
13887
|
+
hookType: isImportedHookType(h.type) ? h.type : "command",
|
|
13888
|
+
converterConfig
|
|
13889
|
+
});
|
|
13890
|
+
if (skipReason !== void 0) {
|
|
13891
|
+
warn?.(skipReason);
|
|
13892
|
+
continue;
|
|
13893
|
+
}
|
|
13894
|
+
definitions.push(toolHookToCanonical({
|
|
13895
|
+
h,
|
|
13896
|
+
rawEntry,
|
|
13897
|
+
converterConfig,
|
|
13898
|
+
warn
|
|
13899
|
+
}));
|
|
13900
|
+
}
|
|
13901
|
+
return definitions;
|
|
13413
13902
|
}
|
|
13414
13903
|
/**
|
|
13415
13904
|
* Assemble the canonical hooks config a tool importer writes to
|
|
@@ -13439,6 +13928,7 @@ function buildImportedHooksConfig({ hooks, overrideKey, version = 1, extraOverri
|
|
|
13439
13928
|
}
|
|
13440
13929
|
function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
13441
13930
|
if (hooks === null || hooks === void 0 || typeof hooks !== "object") return {};
|
|
13931
|
+
const warn = warnOnce(logger);
|
|
13442
13932
|
const canonical = {};
|
|
13443
13933
|
for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
|
|
13444
13934
|
const eventName = converterConfig.toolToCanonicalEventNames[toolEventName] ?? toolEventName;
|
|
@@ -13449,7 +13939,7 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
13449
13939
|
defs.push(...toolMatcherEntryToCanonical({
|
|
13450
13940
|
rawEntry,
|
|
13451
13941
|
converterConfig,
|
|
13452
|
-
|
|
13942
|
+
warn
|
|
13453
13943
|
}));
|
|
13454
13944
|
}
|
|
13455
13945
|
if (defs.length > 0) canonical[eventName] = defs;
|
|
@@ -13488,7 +13978,9 @@ const ANTIGRAVITY_HOOK_NAME = "rulesync";
|
|
|
13488
13978
|
* map for import. Accepts both the documented named-hook shape
|
|
13489
13979
|
* (`{ "<name>": { "<Event>": [...], "enabled"?: bool } }`) and a legacy flat
|
|
13490
13980
|
* shape (`{ "<Event>": [...] }`) so older or hand-written files still import.
|
|
13491
|
-
* The per-hook `enabled` flag is ignored
|
|
13981
|
+
* The per-hook `enabled` flag is ignored. The canonical `enabled` field is a
|
|
13982
|
+
* property of a single hook definition, whereas Antigravity's flag gates a whole
|
|
13983
|
+
* named group, so the two do not map onto each other.
|
|
13492
13984
|
*/
|
|
13493
13985
|
function flattenAntigravityHooks(parsed) {
|
|
13494
13986
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -13570,7 +14062,7 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
13570
14062
|
validate
|
|
13571
14063
|
});
|
|
13572
14064
|
}
|
|
13573
|
-
toRulesyncHooks() {
|
|
14065
|
+
toRulesyncHooks({ logger } = {}) {
|
|
13574
14066
|
let parsed;
|
|
13575
14067
|
try {
|
|
13576
14068
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -13579,7 +14071,8 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
13579
14071
|
}
|
|
13580
14072
|
const hooks = toolHooksToCanonical({
|
|
13581
14073
|
hooks: flattenAntigravityHooks(parsed),
|
|
13582
|
-
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG
|
|
14074
|
+
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
|
|
14075
|
+
logger
|
|
13583
14076
|
});
|
|
13584
14077
|
const overrideKey = this.constructor.getOverrideKey();
|
|
13585
14078
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
@@ -13910,6 +14403,14 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13910
14403
|
isDeletable() {
|
|
13911
14404
|
return false;
|
|
13912
14405
|
}
|
|
14406
|
+
/**
|
|
14407
|
+
* The converter config used for both directions. Exposed as a static hook so
|
|
14408
|
+
* plugin-scoped subclasses can swap tool-specific details (e.g. the project
|
|
14409
|
+
* directory variable) without duplicating the rest of the config.
|
|
14410
|
+
*/
|
|
14411
|
+
static getConverterConfig() {
|
|
14412
|
+
return CLAUDE_CONVERTER_CONFIG;
|
|
14413
|
+
}
|
|
13913
14414
|
static getSettablePaths(_options = {}) {
|
|
13914
14415
|
return {
|
|
13915
14416
|
relativeDirPath: CLAUDECODE_DIR,
|
|
@@ -13935,7 +14436,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13935
14436
|
const claudeHooks = canonicalToToolHooks({
|
|
13936
14437
|
config,
|
|
13937
14438
|
toolOverrideHooks: config.claudecode?.hooks,
|
|
13938
|
-
converterConfig:
|
|
14439
|
+
converterConfig: this.getConverterConfig(),
|
|
13939
14440
|
logger
|
|
13940
14441
|
});
|
|
13941
14442
|
const fileContent = applySharedConfigPatch({
|
|
@@ -13953,7 +14454,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13953
14454
|
validate
|
|
13954
14455
|
});
|
|
13955
14456
|
}
|
|
13956
|
-
toRulesyncHooks() {
|
|
14457
|
+
toRulesyncHooks({ logger } = {}) {
|
|
13957
14458
|
let settings;
|
|
13958
14459
|
try {
|
|
13959
14460
|
settings = JSON.parse(this.getFileContent());
|
|
@@ -13962,7 +14463,8 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13962
14463
|
}
|
|
13963
14464
|
const hooks = toolHooksToCanonical({
|
|
13964
14465
|
hooks: settings.hooks,
|
|
13965
|
-
converterConfig:
|
|
14466
|
+
converterConfig: this.constructor.getConverterConfig(),
|
|
14467
|
+
logger
|
|
13966
14468
|
});
|
|
13967
14469
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
13968
14470
|
hooks,
|
|
@@ -13991,6 +14493,21 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
|
13991
14493
|
isDeletable() {
|
|
13992
14494
|
return true;
|
|
13993
14495
|
}
|
|
14496
|
+
/**
|
|
14497
|
+
* Plugin hook scripts ship inside the plugin, so their commands must resolve
|
|
14498
|
+
* against the plugin install directory rather than the consumer's project
|
|
14499
|
+
* root. Upstream documents `"${CLAUDE_PLUGIN_ROOT}"/scripts/format-code.sh`;
|
|
14500
|
+
* `$CLAUDE_PROJECT_DIR` would expand to a path in the consumer's own repo,
|
|
14501
|
+
* where the bundled script does not exist.
|
|
14502
|
+
*
|
|
14503
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
14504
|
+
*/
|
|
14505
|
+
static getConverterConfig() {
|
|
14506
|
+
return {
|
|
14507
|
+
...super.getConverterConfig(),
|
|
14508
|
+
projectDirVar: "$CLAUDE_PLUGIN_ROOT"
|
|
14509
|
+
};
|
|
14510
|
+
}
|
|
13994
14511
|
static getSettablePaths() {
|
|
13995
14512
|
return {
|
|
13996
14513
|
relativeDirPath: CLAUDECODE_PLUGIN_HOOKS_DIR,
|
|
@@ -14013,6 +14530,10 @@ const CODEXCLI_CONVERTER_CONFIG = {
|
|
|
14013
14530
|
}, {
|
|
14014
14531
|
canonical: "statusMessage",
|
|
14015
14532
|
tool: "statusMessage"
|
|
14533
|
+
}],
|
|
14534
|
+
numberPassthroughFields: [{
|
|
14535
|
+
canonical: "additionalContextLimit",
|
|
14536
|
+
tool: "additionalContextLimit"
|
|
14016
14537
|
}]
|
|
14017
14538
|
};
|
|
14018
14539
|
/**
|
|
@@ -14102,13 +14623,14 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14102
14623
|
validate
|
|
14103
14624
|
});
|
|
14104
14625
|
}
|
|
14105
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
14626
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
14106
14627
|
const paths = CodexcliHooks.getSettablePaths({ global });
|
|
14107
14628
|
const config = rulesyncHooks.getJson();
|
|
14108
14629
|
const codexHooks = canonicalToToolHooks({
|
|
14109
14630
|
config,
|
|
14110
14631
|
toolOverrideHooks: config.codexcli?.hooks,
|
|
14111
|
-
converterConfig: CODEXCLI_CONVERTER_CONFIG
|
|
14632
|
+
converterConfig: CODEXCLI_CONVERTER_CONFIG,
|
|
14633
|
+
logger
|
|
14112
14634
|
});
|
|
14113
14635
|
const fileContent = JSON.stringify({ hooks: codexHooks }, null, 2);
|
|
14114
14636
|
return new CodexcliHooks({
|
|
@@ -14119,7 +14641,7 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14119
14641
|
validate
|
|
14120
14642
|
});
|
|
14121
14643
|
}
|
|
14122
|
-
toRulesyncHooks() {
|
|
14644
|
+
toRulesyncHooks({ logger } = {}) {
|
|
14123
14645
|
let parsed;
|
|
14124
14646
|
try {
|
|
14125
14647
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -14128,7 +14650,8 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14128
14650
|
}
|
|
14129
14651
|
const hooks = toolHooksToCanonical({
|
|
14130
14652
|
hooks: parsed.hooks,
|
|
14131
|
-
converterConfig: CODEXCLI_CONVERTER_CONFIG
|
|
14653
|
+
converterConfig: CODEXCLI_CONVERTER_CONFIG,
|
|
14654
|
+
logger
|
|
14132
14655
|
});
|
|
14133
14656
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
14134
14657
|
hooks,
|
|
@@ -14367,13 +14890,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
14367
14890
|
* under `.github/hooks/` is picked up automatically when the CLI is
|
|
14368
14891
|
* invoked from the project root.
|
|
14369
14892
|
*
|
|
14370
|
-
* - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` —
|
|
14371
|
-
*
|
|
14372
|
-
*
|
|
14373
|
-
*
|
|
14374
|
-
*
|
|
14375
|
-
*
|
|
14376
|
-
*
|
|
14893
|
+
* - **Global scope**: `~/.copilot/hooks/copilot-hooks.json` — the directory is
|
|
14894
|
+
* the documented user-level hooks location ("`*.json` files in the
|
|
14895
|
+
* user-level hooks directory. By default this is `~/.copilot/hooks/` on
|
|
14896
|
+
* macOS and Linux, or `%USERPROFILE%\.copilot\hooks\` on Windows"). Every
|
|
14897
|
+
* `*.json` in it is loaded, so the filename remains rulesync's choice, as it
|
|
14898
|
+
* is for project scope. `COPILOT_HOME` relocates the directory upstream
|
|
14899
|
+
* (`$COPILOT_HOME/hooks/`); rulesync does not read that variable yet.
|
|
14377
14900
|
*
|
|
14378
14901
|
* Hook entries on the six matcher-aware events (see
|
|
14379
14902
|
* {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
|
|
@@ -14784,17 +15307,18 @@ const DEEPAGENTS_MCP_FILE_NAME = ".mcp.json";
|
|
|
14784
15307
|
const DEEPAGENTS_HOOKS_FILE_NAME = "hooks.json";
|
|
14785
15308
|
//#endregion
|
|
14786
15309
|
//#region src/features/hooks/deepagents-hooks.ts
|
|
14787
|
-
function
|
|
14788
|
-
|
|
14789
|
-
return Array.isArray(val.hooks);
|
|
15310
|
+
function isRecord(value) {
|
|
15311
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14790
15312
|
}
|
|
14791
15313
|
/**
|
|
14792
|
-
* Convert canonical hooks config to deepagents
|
|
15314
|
+
* Convert the canonical hooks config to the deepagents Hooks v2 document.
|
|
14793
15315
|
*
|
|
14794
|
-
*
|
|
14795
|
-
* { "hooks":
|
|
15316
|
+
* ```json
|
|
15317
|
+
* { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "…" }] }] } }
|
|
15318
|
+
* ```
|
|
14796
15319
|
*
|
|
14797
|
-
*
|
|
15320
|
+
* Definitions sharing an event and matcher land in one group, preserving their
|
|
15321
|
+
* authored order — upstream runs a group's handlers in sequence.
|
|
14798
15322
|
*/
|
|
14799
15323
|
function canonicalToDeepagentsHooks(config) {
|
|
14800
15324
|
const supported = new Set(DEEPAGENTS_HOOK_EVENTS);
|
|
@@ -14802,7 +15326,7 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
14802
15326
|
...config.hooks,
|
|
14803
15327
|
...config.deepagents?.hooks
|
|
14804
15328
|
};
|
|
14805
|
-
const
|
|
15329
|
+
const hooks = {};
|
|
14806
15330
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
14807
15331
|
if (!supported.has(canonicalEvent)) continue;
|
|
14808
15332
|
const deepagentsEvent = CANONICAL_TO_DEEPAGENTS_EVENT_NAMES[canonicalEvent];
|
|
@@ -14810,43 +15334,69 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
14810
15334
|
for (const def of definitions) {
|
|
14811
15335
|
if ((def.type ?? "command") !== "command") continue;
|
|
14812
15336
|
if (!def.command) continue;
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
command:
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
15337
|
+
const handler = {
|
|
15338
|
+
type: "command",
|
|
15339
|
+
command: def.command
|
|
15340
|
+
};
|
|
15341
|
+
if (def.timeout !== void 0 && def.timeout !== null && def.timeout > 0) handler.timeout = def.timeout;
|
|
15342
|
+
if (def.statusMessage !== void 0 && def.statusMessage !== null) handler.statusMessage = def.statusMessage;
|
|
15343
|
+
const matcher = def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" ? def.matcher : void 0;
|
|
15344
|
+
const groups = hooks[deepagentsEvent] ??= [];
|
|
15345
|
+
const group = groups.find((candidate) => candidate.matcher === matcher);
|
|
15346
|
+
if (group) group.hooks.push(handler);
|
|
15347
|
+
else groups.push({
|
|
15348
|
+
...matcher !== void 0 && { matcher },
|
|
15349
|
+
hooks: [handler]
|
|
14821
15350
|
});
|
|
14822
15351
|
}
|
|
14823
15352
|
}
|
|
14824
|
-
return
|
|
15353
|
+
return hooks;
|
|
15354
|
+
}
|
|
15355
|
+
/**
|
|
15356
|
+
* Convert the Hooks v2 document back to the canonical hooks record.
|
|
15357
|
+
*/
|
|
15358
|
+
function deepagentsToCanonicalHooks(hooks) {
|
|
15359
|
+
const canonical = {};
|
|
15360
|
+
for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
|
|
15361
|
+
const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
|
|
15362
|
+
if (!canonicalEvent || !Array.isArray(groups)) continue;
|
|
15363
|
+
for (const group of groups) {
|
|
15364
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
15365
|
+
for (const handler of group.hooks) {
|
|
15366
|
+
if (!isRecord(handler) || typeof handler.command !== "string") continue;
|
|
15367
|
+
const def = {
|
|
15368
|
+
type: "command",
|
|
15369
|
+
command: handler.command
|
|
15370
|
+
};
|
|
15371
|
+
if (typeof group.matcher === "string" && group.matcher !== "") def.matcher = group.matcher;
|
|
15372
|
+
if (typeof handler.timeout === "number") def.timeout = handler.timeout;
|
|
15373
|
+
if (typeof handler.statusMessage === "string") def.statusMessage = handler.statusMessage;
|
|
15374
|
+
(canonical[canonicalEvent] ??= []).push(def);
|
|
15375
|
+
}
|
|
15376
|
+
}
|
|
15377
|
+
}
|
|
15378
|
+
return canonical;
|
|
14825
15379
|
}
|
|
14826
15380
|
/**
|
|
14827
|
-
*
|
|
15381
|
+
* Read the pre-v2 flat list. deepagents still loads it until 2026-09-01, so a
|
|
15382
|
+
* `hooks.json` a user has not migrated yet is imported rather than discarded —
|
|
15383
|
+
* but rulesync only ever writes the v2 shape, so regenerating migrates it.
|
|
14828
15384
|
*/
|
|
14829
|
-
function
|
|
15385
|
+
function deepagentsLegacyToCanonicalHooks(entries) {
|
|
14830
15386
|
const canonical = {};
|
|
14831
|
-
for (const entry of
|
|
14832
|
-
if (
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
const
|
|
14838
|
-
|
|
14839
|
-
const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
|
|
15387
|
+
for (const entry of entries) {
|
|
15388
|
+
if (!isRecord(entry)) continue;
|
|
15389
|
+
const argv = entry.command;
|
|
15390
|
+
if (!Array.isArray(argv) || argv.length === 0) continue;
|
|
15391
|
+
const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
|
|
15392
|
+
const events = Array.isArray(entry.events) ? entry.events : [];
|
|
15393
|
+
for (const legacyEvent of events) {
|
|
15394
|
+
const canonicalEvent = typeof legacyEvent === "string" ? DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES[legacyEvent] : void 0;
|
|
14840
15395
|
if (!canonicalEvent) continue;
|
|
14841
|
-
|
|
14842
|
-
if (existing) existing.push({
|
|
15396
|
+
(canonical[canonicalEvent] ??= []).push({
|
|
14843
15397
|
type: "command",
|
|
14844
15398
|
command
|
|
14845
15399
|
});
|
|
14846
|
-
else canonical[canonicalEvent] = [{
|
|
14847
|
-
type: "command",
|
|
14848
|
-
command
|
|
14849
|
-
}];
|
|
14850
15400
|
}
|
|
14851
15401
|
}
|
|
14852
15402
|
return canonical;
|
|
@@ -14855,7 +15405,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14855
15405
|
constructor(params) {
|
|
14856
15406
|
super({
|
|
14857
15407
|
...params,
|
|
14858
|
-
fileContent: params.fileContent ?? JSON.stringify({ hooks:
|
|
15408
|
+
fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
|
|
14859
15409
|
});
|
|
14860
15410
|
}
|
|
14861
15411
|
isDeletable() {
|
|
@@ -14869,7 +15419,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14869
15419
|
}
|
|
14870
15420
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14871
15421
|
const paths = DeepagentsHooks.getSettablePaths({ global });
|
|
14872
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks:
|
|
15422
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} }, null, 2);
|
|
14873
15423
|
return new DeepagentsHooks({
|
|
14874
15424
|
outputRoot,
|
|
14875
15425
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -14897,7 +15447,8 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14897
15447
|
} catch (error) {
|
|
14898
15448
|
throw new Error(`Failed to parse deepagents hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
14899
15449
|
}
|
|
14900
|
-
const
|
|
15450
|
+
const rawHooks = isRecord(parsed) ? parsed.hooks : void 0;
|
|
15451
|
+
const hooks = Array.isArray(rawHooks) ? deepagentsLegacyToCanonicalHooks(rawHooks) : isRecord(rawHooks) ? deepagentsToCanonicalHooks(rawHooks) : {};
|
|
14901
15452
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
14902
15453
|
hooks,
|
|
14903
15454
|
overrideKey: "deepagents"
|
|
@@ -14914,7 +15465,7 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
14914
15465
|
outputRoot,
|
|
14915
15466
|
relativeDirPath,
|
|
14916
15467
|
relativeFilePath,
|
|
14917
|
-
fileContent: JSON.stringify({ hooks:
|
|
15468
|
+
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
14918
15469
|
validate: false
|
|
14919
15470
|
});
|
|
14920
15471
|
}
|
|
@@ -15016,7 +15567,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
15016
15567
|
validate
|
|
15017
15568
|
});
|
|
15018
15569
|
}
|
|
15019
|
-
toRulesyncHooks() {
|
|
15570
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15020
15571
|
let parsed;
|
|
15021
15572
|
try {
|
|
15022
15573
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -15024,8 +15575,9 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
15024
15575
|
throw new Error(`Failed to parse Devin hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
15025
15576
|
}
|
|
15026
15577
|
const hooks = toolHooksToCanonical({
|
|
15027
|
-
hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
15028
|
-
converterConfig: DEVIN_CONVERTER_CONFIG
|
|
15578
|
+
hooks: this.getRelativeFilePath() === "config.json" ? isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
15579
|
+
converterConfig: DEVIN_CONVERTER_CONFIG,
|
|
15580
|
+
logger
|
|
15029
15581
|
});
|
|
15030
15582
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15031
15583
|
hooks,
|
|
@@ -15056,7 +15608,13 @@ const FACTORYDROID_CONVERTER_CONFIG = {
|
|
|
15056
15608
|
toolToCanonicalEventNames: FACTORYDROID_TO_CANONICAL_EVENT_NAMES,
|
|
15057
15609
|
projectDirVar: "$FACTORY_PROJECT_DIR",
|
|
15058
15610
|
prefixDotRelativeCommandsOnly: true,
|
|
15059
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15611
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15612
|
+
groupPassthroughFields: [{
|
|
15613
|
+
canonical: "commandRegex",
|
|
15614
|
+
tool: "commandRegex",
|
|
15615
|
+
valueType: "string",
|
|
15616
|
+
subdividesGroup: true
|
|
15617
|
+
}]
|
|
15060
15618
|
};
|
|
15061
15619
|
var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
15062
15620
|
constructor(params) {
|
|
@@ -15113,7 +15671,7 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
15113
15671
|
validate
|
|
15114
15672
|
});
|
|
15115
15673
|
}
|
|
15116
|
-
toRulesyncHooks() {
|
|
15674
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15117
15675
|
let settings;
|
|
15118
15676
|
try {
|
|
15119
15677
|
settings = JSON.parse(this.getFileContent());
|
|
@@ -15122,7 +15680,8 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
15122
15680
|
}
|
|
15123
15681
|
const hooks = toolHooksToCanonical({
|
|
15124
15682
|
hooks: settings.hooks,
|
|
15125
|
-
converterConfig: FACTORYDROID_CONVERTER_CONFIG
|
|
15683
|
+
converterConfig: FACTORYDROID_CONVERTER_CONFIG,
|
|
15684
|
+
logger
|
|
15126
15685
|
});
|
|
15127
15686
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15128
15687
|
hooks,
|
|
@@ -15152,7 +15711,8 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
15152
15711
|
canonicalToToolEventNames: CANONICAL_TO_GOOSE_EVENT_NAMES,
|
|
15153
15712
|
toolToCanonicalEventNames: GOOSE_TO_CANONICAL_EVENT_NAMES,
|
|
15154
15713
|
projectDirVar: "",
|
|
15155
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15714
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15715
|
+
wildcardMatcherMeansAll: true
|
|
15156
15716
|
};
|
|
15157
15717
|
/**
|
|
15158
15718
|
* Represents a Goose lifecycle hooks file.
|
|
@@ -15190,13 +15750,14 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
15190
15750
|
validate
|
|
15191
15751
|
});
|
|
15192
15752
|
}
|
|
15193
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
15753
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
15194
15754
|
const paths = GooseHooks.getSettablePaths({ global });
|
|
15195
15755
|
const config = rulesyncHooks.getJson();
|
|
15196
15756
|
const gooseHooks = canonicalToToolHooks({
|
|
15197
15757
|
config,
|
|
15198
15758
|
toolOverrideHooks: config.goose?.hooks,
|
|
15199
|
-
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
15759
|
+
converterConfig: GOOSE_CONVERTER_CONFIG,
|
|
15760
|
+
logger
|
|
15200
15761
|
});
|
|
15201
15762
|
const fileContent = JSON.stringify({ hooks: gooseHooks }, null, 2);
|
|
15202
15763
|
return new GooseHooks({
|
|
@@ -15207,7 +15768,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
15207
15768
|
validate
|
|
15208
15769
|
});
|
|
15209
15770
|
}
|
|
15210
|
-
toRulesyncHooks() {
|
|
15771
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15211
15772
|
let parsed;
|
|
15212
15773
|
try {
|
|
15213
15774
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -15216,7 +15777,8 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
15216
15777
|
}
|
|
15217
15778
|
const hooks = toolHooksToCanonical({
|
|
15218
15779
|
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,
|
|
15219
|
-
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
15780
|
+
converterConfig: GOOSE_CONVERTER_CONFIG,
|
|
15781
|
+
logger
|
|
15220
15782
|
});
|
|
15221
15783
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15222
15784
|
hooks,
|
|
@@ -15247,6 +15809,11 @@ const GROKCLI_CONVERTER_CONFIG = {
|
|
|
15247
15809
|
toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
|
|
15248
15810
|
projectDirVar: "",
|
|
15249
15811
|
supportedHookTypes: /* @__PURE__ */ new Set(["command", "http"]),
|
|
15812
|
+
recordPassthroughFields: [{
|
|
15813
|
+
canonical: "env",
|
|
15814
|
+
tool: "env",
|
|
15815
|
+
commandOnly: true
|
|
15816
|
+
}],
|
|
15250
15817
|
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
15251
15818
|
"sessionStart",
|
|
15252
15819
|
"sessionEnd",
|
|
@@ -15315,7 +15882,7 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
|
|
|
15315
15882
|
validate
|
|
15316
15883
|
});
|
|
15317
15884
|
}
|
|
15318
|
-
toRulesyncHooks() {
|
|
15885
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15319
15886
|
let parsed;
|
|
15320
15887
|
try {
|
|
15321
15888
|
parsed = JSON.parse(this.getFileContent());
|
|
@@ -15323,8 +15890,9 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
|
|
|
15323
15890
|
throw new Error(`Failed to parse Grok hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
15324
15891
|
}
|
|
15325
15892
|
const hooks = toolHooksToCanonical({
|
|
15326
|
-
hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
|
|
15327
|
-
converterConfig: GROKCLI_CONVERTER_CONFIG
|
|
15893
|
+
hooks: isRecord$1(parsed) && isRecord$1(parsed.hooks) ? parsed.hooks : {},
|
|
15894
|
+
converterConfig: GROKCLI_CONVERTER_CONFIG,
|
|
15895
|
+
logger
|
|
15328
15896
|
});
|
|
15329
15897
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15330
15898
|
hooks,
|
|
@@ -15645,7 +16213,7 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
15645
16213
|
validate
|
|
15646
16214
|
});
|
|
15647
16215
|
}
|
|
15648
|
-
toRulesyncHooks() {
|
|
16216
|
+
toRulesyncHooks({ logger } = {}) {
|
|
15649
16217
|
let settings;
|
|
15650
16218
|
try {
|
|
15651
16219
|
settings = JSON.parse(this.getFileContent());
|
|
@@ -15654,7 +16222,8 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
15654
16222
|
}
|
|
15655
16223
|
const hooks = toolHooksToCanonical({
|
|
15656
16224
|
hooks: settings.hooks,
|
|
15657
|
-
converterConfig: JUNIE_CONVERTER_CONFIG
|
|
16225
|
+
converterConfig: JUNIE_CONVERTER_CONFIG,
|
|
16226
|
+
logger
|
|
15658
16227
|
});
|
|
15659
16228
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
15660
16229
|
hooks,
|
|
@@ -16392,7 +16961,7 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
|
16392
16961
|
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
16393
16962
|
action,
|
|
16394
16963
|
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
16395
|
-
enabled: true
|
|
16964
|
+
enabled: def.enabled ?? true
|
|
16396
16965
|
});
|
|
16397
16966
|
}
|
|
16398
16967
|
return entries;
|
|
@@ -16432,6 +17001,7 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
16432
17001
|
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
16433
17002
|
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
16434
17003
|
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
17004
|
+
if (entry.enabled === false) def.enabled = false;
|
|
16435
17005
|
(canonical[eventName] ??= []).push(def);
|
|
16436
17006
|
}
|
|
16437
17007
|
return canonical;
|
|
@@ -17376,6 +17946,37 @@ function unsupportedEventNames(params) {
|
|
|
17376
17946
|
const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
|
|
17377
17947
|
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
17378
17948
|
}
|
|
17949
|
+
/**
|
|
17950
|
+
* A logger whose warnings name the tool being converted, matching the rest of
|
|
17951
|
+
* this processor's warnings: the shared hooks converter says what is wrong but
|
|
17952
|
+
* not which tool's file it is reading or writing, and one run walks them all.
|
|
17953
|
+
*
|
|
17954
|
+
* Every method delegates explicitly rather than through a prototype, so the
|
|
17955
|
+
* real logger keeps owning its state — a wrapper that inherited it would
|
|
17956
|
+
* absorb the writes `configure` and `outputJson` make.
|
|
17957
|
+
*/
|
|
17958
|
+
function withToolTargetPrefix({ logger, toolTarget }) {
|
|
17959
|
+
return {
|
|
17960
|
+
configure: (options) => logger.configure(options),
|
|
17961
|
+
get verbose() {
|
|
17962
|
+
return logger.verbose;
|
|
17963
|
+
},
|
|
17964
|
+
get silent() {
|
|
17965
|
+
return logger.silent;
|
|
17966
|
+
},
|
|
17967
|
+
get jsonMode() {
|
|
17968
|
+
return logger.jsonMode;
|
|
17969
|
+
},
|
|
17970
|
+
captureData: (key, value) => logger.captureData(key, value),
|
|
17971
|
+
getJsonData: () => logger.getJsonData(),
|
|
17972
|
+
outputJson: (success, error) => logger.outputJson(success, error),
|
|
17973
|
+
info: (message, ...args) => logger.info(message, ...args),
|
|
17974
|
+
success: (message, ...args) => logger.success(message, ...args),
|
|
17975
|
+
warn: (message, ...args) => logger.warn(`For ${toolTarget}: ${message}`, ...args),
|
|
17976
|
+
error: (message, code, ...args) => logger.error(message, code, ...args),
|
|
17977
|
+
debug: (message, ...args) => logger.debug(message, ...args)
|
|
17978
|
+
};
|
|
17979
|
+
}
|
|
17379
17980
|
function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
|
|
17380
17981
|
if (factory.supportsMatcher && !factory.matcherEvents) return [];
|
|
17381
17982
|
const matcherEvents = factory.matcherEvents ? new Set(factory.matcherEvents) : void 0;
|
|
@@ -17598,13 +18199,13 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
17598
18199
|
["deepagents", {
|
|
17599
18200
|
class: DeepagentsHooks,
|
|
17600
18201
|
meta: {
|
|
17601
|
-
supportsProject:
|
|
18202
|
+
supportsProject: true,
|
|
17602
18203
|
supportsGlobal: true,
|
|
17603
18204
|
supportsImport: true
|
|
17604
18205
|
},
|
|
17605
18206
|
supportedEvents: DEEPAGENTS_HOOK_EVENTS,
|
|
17606
18207
|
supportedHookTypes: ["command"],
|
|
17607
|
-
supportsMatcher:
|
|
18208
|
+
supportsMatcher: true
|
|
17608
18209
|
}],
|
|
17609
18210
|
["kiro", {
|
|
17610
18211
|
class: KiroHooks,
|
|
@@ -17816,6 +18417,15 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
17816
18417
|
}
|
|
17817
18418
|
for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
|
|
17818
18419
|
}
|
|
18420
|
+
if (this.toolTarget !== "kiro-ide") {
|
|
18421
|
+
const skippedEvents = new Set(unsupportedEventNames({
|
|
18422
|
+
factory,
|
|
18423
|
+
sharedHooks,
|
|
18424
|
+
effectiveHooks
|
|
18425
|
+
}));
|
|
18426
|
+
const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
|
|
18427
|
+
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(", ")}`);
|
|
18428
|
+
}
|
|
17819
18429
|
const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
|
|
17820
18430
|
factory,
|
|
17821
18431
|
effectiveHooks
|
|
@@ -17826,7 +18436,10 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
17826
18436
|
rulesyncHooks,
|
|
17827
18437
|
validate: true,
|
|
17828
18438
|
global: this.global,
|
|
17829
|
-
logger:
|
|
18439
|
+
logger: withToolTargetPrefix({
|
|
18440
|
+
logger: this.logger,
|
|
18441
|
+
toolTarget: this.toolTarget
|
|
18442
|
+
})
|
|
17830
18443
|
})];
|
|
17831
18444
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
17832
18445
|
outputRoot: this.outputRoot,
|
|
@@ -17836,7 +18449,12 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
17836
18449
|
return result;
|
|
17837
18450
|
}
|
|
17838
18451
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
17839
|
-
|
|
18452
|
+
const hooks = toolFiles.filter((f) => f instanceof ToolHooks);
|
|
18453
|
+
const logger = withToolTargetPrefix({
|
|
18454
|
+
logger: this.logger,
|
|
18455
|
+
toolTarget: this.toolTarget
|
|
18456
|
+
});
|
|
18457
|
+
return hooks.map((h) => h.toRulesyncHooks({ logger }));
|
|
17840
18458
|
}
|
|
17841
18459
|
static getToolTargets({ global = false, importOnly = false } = {}) {
|
|
17842
18460
|
if (global) return importOnly ? hooksProcessorToolTargetsGlobalImportable : hooksProcessorToolTargetsGlobal;
|
|
@@ -19479,9 +20097,9 @@ function parseAmpSettingsJsonc(fileContent) {
|
|
|
19479
20097
|
}
|
|
19480
20098
|
function filterMcpServers(mcpServers) {
|
|
19481
20099
|
const filtered = {};
|
|
19482
|
-
if (!isRecord(mcpServers)) return filtered;
|
|
20100
|
+
if (!isRecord$1(mcpServers)) return filtered;
|
|
19483
20101
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
19484
|
-
if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
|
|
20102
|
+
if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
|
|
19485
20103
|
const filteredConfig = {};
|
|
19486
20104
|
for (const [key, value] of Object.entries(config)) {
|
|
19487
20105
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -19596,7 +20214,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
19596
20214
|
success: true,
|
|
19597
20215
|
error: null
|
|
19598
20216
|
};
|
|
19599
|
-
if (!isRecord(mcpServers)) return {
|
|
20217
|
+
if (!isRecord$1(mcpServers)) return {
|
|
19600
20218
|
success: false,
|
|
19601
20219
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
19602
20220
|
};
|
|
@@ -19605,7 +20223,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
19605
20223
|
success: false,
|
|
19606
20224
|
error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
|
|
19607
20225
|
};
|
|
19608
|
-
if (!isRecord(serverConfig)) return {
|
|
20226
|
+
if (!isRecord$1(serverConfig)) return {
|
|
19609
20227
|
success: false,
|
|
19610
20228
|
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
19611
20229
|
};
|
|
@@ -20205,13 +20823,13 @@ function normalizeCodexMcpServerName(name) {
|
|
|
20205
20823
|
function convertFromCodexFormat(codexMcp) {
|
|
20206
20824
|
const result = {};
|
|
20207
20825
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
20208
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
20826
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
20209
20827
|
const converted = {};
|
|
20210
20828
|
for (const [key, value] of Object.entries(config)) {
|
|
20211
20829
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20212
20830
|
if (key === "enabled") {
|
|
20213
20831
|
if (value === false) converted["disabled"] = true;
|
|
20214
|
-
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
|
|
20832
|
+
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
|
|
20215
20833
|
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
20216
20834
|
const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
|
|
20217
20835
|
if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
|
|
@@ -20230,7 +20848,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
20230
20848
|
const result = {};
|
|
20231
20849
|
const originalNames = /* @__PURE__ */ new Map();
|
|
20232
20850
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
20233
|
-
if (!isRecord(config)) continue;
|
|
20851
|
+
if (!isRecord$1(config)) continue;
|
|
20234
20852
|
const { codexName, usedFallback } = normalizeCodexMcpServerName(name);
|
|
20235
20853
|
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.`);
|
|
20236
20854
|
const converted = {};
|
|
@@ -20238,7 +20856,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
20238
20856
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20239
20857
|
if (key === "disabled") {
|
|
20240
20858
|
if (value === true) converted["enabled"] = false;
|
|
20241
|
-
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
|
|
20859
|
+
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
|
|
20242
20860
|
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
20243
20861
|
const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
|
|
20244
20862
|
if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
|
|
@@ -20314,21 +20932,21 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
20314
20932
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
20315
20933
|
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
20316
20934
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
20317
|
-
const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
20935
|
+
const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
20318
20936
|
return [serverName, {
|
|
20319
20937
|
...serverConfig,
|
|
20320
|
-
...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
20321
|
-
...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
|
|
20322
|
-
...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
|
|
20938
|
+
...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
20939
|
+
...isRecord$1(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
|
|
20940
|
+
...isRecord$1(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
|
|
20323
20941
|
}];
|
|
20324
20942
|
})));
|
|
20325
20943
|
const filteredMcpServers = this.removeEmptyEntries(converted);
|
|
20326
20944
|
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`);
|
|
20327
|
-
const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
20945
|
+
const existingMcpServers = isRecord$1(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
20328
20946
|
const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
|
|
20329
|
-
const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
20947
|
+
const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
20330
20948
|
const serverRecord = serverConfig;
|
|
20331
|
-
if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
20949
|
+
if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
20332
20950
|
...serverRecord,
|
|
20333
20951
|
tools: existingServer["tools"]
|
|
20334
20952
|
}];
|
|
@@ -21197,6 +21815,27 @@ function resolveGooseType(config, url) {
|
|
|
21197
21815
|
return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
|
|
21198
21816
|
}
|
|
21199
21817
|
/**
|
|
21818
|
+
* The Goose extension types that carry an MCP server. Goose also documents
|
|
21819
|
+
* `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
|
|
21820
|
+
* no canonical MCP counterpart: they name capabilities Goose provides itself
|
|
21821
|
+
* rather than a server rulesync could describe.
|
|
21822
|
+
*/
|
|
21823
|
+
const GOOSE_MCP_EXTENSION_TYPES = /* @__PURE__ */ new Set([
|
|
21824
|
+
"stdio",
|
|
21825
|
+
"streamable_http",
|
|
21826
|
+
"sse"
|
|
21827
|
+
]);
|
|
21828
|
+
/**
|
|
21829
|
+
* Resolves the Goose extension type of an existing `extensions:` entry the way
|
|
21830
|
+
* Goose itself reads it: the declared `type`, or the shape of the entry when
|
|
21831
|
+
* the key is absent.
|
|
21832
|
+
*/
|
|
21833
|
+
function existingExtensionType(ext) {
|
|
21834
|
+
if (typeof ext.type === "string") return ext.type;
|
|
21835
|
+
if (typeof ext.cmd === "string") return "stdio";
|
|
21836
|
+
if (typeof ext.uri === "string") return "streamable_http";
|
|
21837
|
+
}
|
|
21838
|
+
/**
|
|
21200
21839
|
* Resolves the canonical timeout for a server (`timeout` or `networkTimeout`).
|
|
21201
21840
|
*/
|
|
21202
21841
|
function resolveGooseTimeout(config) {
|
|
@@ -21222,15 +21861,20 @@ function applyGooseStdioFields(ext, config) {
|
|
|
21222
21861
|
/**
|
|
21223
21862
|
* Converts a single rulesync canonical MCP server into a Goose `extensions:` entry.
|
|
21224
21863
|
*/
|
|
21225
|
-
function convertServerToGooseExtension(name, config) {
|
|
21864
|
+
function convertServerToGooseExtension(name, config, logger) {
|
|
21226
21865
|
const url = resolveGooseUrl(config);
|
|
21227
21866
|
const gooseType = resolveGooseType(config, url);
|
|
21228
21867
|
const ext = {
|
|
21229
21868
|
name,
|
|
21230
21869
|
type: gooseType
|
|
21231
21870
|
};
|
|
21232
|
-
if (gooseType === "stdio")
|
|
21233
|
-
|
|
21871
|
+
if (gooseType === "stdio") {
|
|
21872
|
+
applyGooseStdioFields(ext, config);
|
|
21873
|
+
if (typeof ext.cmd !== "string" || ext.cmd === "") {
|
|
21874
|
+
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.`);
|
|
21875
|
+
return;
|
|
21876
|
+
}
|
|
21877
|
+
} else if (gooseType === "sse" || gooseType === "streamable_http") {
|
|
21234
21878
|
if (url !== void 0) ext.uri = url;
|
|
21235
21879
|
if (isPlainObject$1(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
|
|
21236
21880
|
}
|
|
@@ -21245,13 +21889,41 @@ function convertServerToGooseExtension(name, config) {
|
|
|
21245
21889
|
* Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
|
|
21246
21890
|
* | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
|
|
21247
21891
|
* remote, plus `enabled` and `timeout`.
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21892
|
+
*
|
|
21893
|
+
* `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
|
|
21894
|
+
* holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
|
|
21895
|
+
* (`developer`, `memory`, ...), which have no canonical MCP representation.
|
|
21896
|
+
* Those entries are carried over from `existingExtensions` untouched — removing
|
|
21897
|
+
* `developer` alone costs the agent its shell and text-editor tools. Only an
|
|
21898
|
+
* entry rulesync can positively identify as an MCP server is rulesync's to
|
|
21899
|
+
* replace, so a server deleted from `.rulesync/.mcp.json` is retracted (with a
|
|
21900
|
+
* warning naming it) while an entry of an unrecognized shape or a future
|
|
21901
|
+
* extension type is left alone rather than assumed to be ours.
|
|
21902
|
+
*/
|
|
21903
|
+
function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
|
|
21904
|
+
const generated = {};
|
|
21251
21905
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21252
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
21253
|
-
|
|
21906
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21907
|
+
const ext = convertServerToGooseExtension(name, config, logger);
|
|
21908
|
+
if (ext !== void 0) generated[name] = ext;
|
|
21254
21909
|
}
|
|
21910
|
+
const extensions = {};
|
|
21911
|
+
const retracted = [];
|
|
21912
|
+
for (const [name, ext] of Object.entries(existingExtensions)) {
|
|
21913
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
21914
|
+
const type = isRecord$1(ext) ? existingExtensionType(ext) : void 0;
|
|
21915
|
+
if (type !== void 0 && GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21916
|
+
if (!Object.hasOwn(generated, name)) retracted.push(name);
|
|
21917
|
+
continue;
|
|
21918
|
+
}
|
|
21919
|
+
if (!Object.hasOwn(generated, name)) {
|
|
21920
|
+
extensions[name] = ext;
|
|
21921
|
+
continue;
|
|
21922
|
+
}
|
|
21923
|
+
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.`);
|
|
21924
|
+
}
|
|
21925
|
+
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\`.`);
|
|
21926
|
+
Object.assign(extensions, generated);
|
|
21255
21927
|
return extensions;
|
|
21256
21928
|
}
|
|
21257
21929
|
/**
|
|
@@ -21262,16 +21934,27 @@ function convertToGooseFormat(mcpServers) {
|
|
|
21262
21934
|
* so both `url` and the Claude-specific `httpUrl` alias come back as `url`; and
|
|
21263
21935
|
* the `streamable_http` type maps back to canonical `http`. These are the
|
|
21264
21936
|
* canonical/preferred forms, so re-generating produces an equivalent config.
|
|
21937
|
+
*
|
|
21938
|
+
* Non-MCP extension types (`builtin`, `platform`, `frontend`, `inline_python`)
|
|
21939
|
+
* are skipped: they describe capabilities Goose provides itself, and importing
|
|
21940
|
+
* one would strip the type that makes it work — a `builtin` entry came back as
|
|
21941
|
+
* a `stdio` extension with no `cmd` that Goose cannot start. They stay in `config.yaml`,
|
|
21942
|
+
* which generation preserves.
|
|
21265
21943
|
*/
|
|
21266
21944
|
function convertFromGooseFormat(extensions) {
|
|
21267
21945
|
const result = {};
|
|
21946
|
+
const skipped = [];
|
|
21268
21947
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
21269
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
21948
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
|
|
21949
|
+
const type = existingExtensionType(ext);
|
|
21950
|
+
if (type === void 0 || !GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21951
|
+
skipped.push(name);
|
|
21952
|
+
continue;
|
|
21953
|
+
}
|
|
21270
21954
|
const server = {};
|
|
21271
|
-
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
21272
21955
|
if (type === "sse") server.type = "sse";
|
|
21273
21956
|
else if (type === "streamable_http") server.type = "http";
|
|
21274
|
-
else
|
|
21957
|
+
else server.type = "stdio";
|
|
21275
21958
|
if (typeof ext.cmd === "string") server.command = ext.cmd;
|
|
21276
21959
|
if (isStringArray$1(ext.args)) server.args = ext.args;
|
|
21277
21960
|
if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
|
|
@@ -21281,6 +21964,7 @@ function convertFromGooseFormat(extensions) {
|
|
|
21281
21964
|
if (typeof ext.timeout === "number") server.timeout = ext.timeout;
|
|
21282
21965
|
result[name] = server;
|
|
21283
21966
|
}
|
|
21967
|
+
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.`);
|
|
21284
21968
|
return result;
|
|
21285
21969
|
}
|
|
21286
21970
|
/**
|
|
@@ -21315,7 +21999,7 @@ function buildGoosePluginStdioServer(config) {
|
|
|
21315
21999
|
function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
21316
22000
|
const result = {};
|
|
21317
22001
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21318
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22002
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21319
22003
|
const gooseType = resolveGooseType(config, resolveGooseUrl(config));
|
|
21320
22004
|
if (gooseType !== "stdio") {
|
|
21321
22005
|
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.`);
|
|
@@ -21356,7 +22040,7 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
21356
22040
|
parsePluginManifest(fileContent) {
|
|
21357
22041
|
try {
|
|
21358
22042
|
const parsed = JSON.parse(fileContent);
|
|
21359
|
-
return isRecord(parsed) ? parsed : {};
|
|
22043
|
+
return isRecord$1(parsed) ? parsed : {};
|
|
21360
22044
|
} catch (error) {
|
|
21361
22045
|
throw new Error(`Failed to parse Goose MCP manifest at ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
21362
22046
|
}
|
|
@@ -21402,25 +22086,35 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
21402
22086
|
global
|
|
21403
22087
|
});
|
|
21404
22088
|
}
|
|
21405
|
-
const
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
};
|
|
22089
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
22090
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
22091
|
+
const config = parseGooseConfig(existingContent, paths.relativeDirPath, paths.relativeFilePath);
|
|
22092
|
+
const existingExtensions = isRecord$1(config.extensions) ? config.extensions : {};
|
|
21409
22093
|
return new GooseMcp({
|
|
21410
22094
|
outputRoot,
|
|
21411
22095
|
relativeDirPath: paths.relativeDirPath,
|
|
21412
22096
|
relativeFilePath: paths.relativeFilePath,
|
|
21413
|
-
fileContent:
|
|
22097
|
+
fileContent: applySharedConfigPatch({
|
|
22098
|
+
fileKey: sharedConfigFileKey(paths),
|
|
22099
|
+
feature: "mcp",
|
|
22100
|
+
existingContent,
|
|
22101
|
+
patch: { extensions: convertToGooseFormat({
|
|
22102
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
22103
|
+
existingExtensions,
|
|
22104
|
+
logger
|
|
22105
|
+
}) },
|
|
22106
|
+
filePath
|
|
22107
|
+
}),
|
|
21414
22108
|
validate,
|
|
21415
22109
|
global
|
|
21416
22110
|
});
|
|
21417
22111
|
}
|
|
21418
22112
|
toRulesyncMcp() {
|
|
21419
22113
|
if (!this.global) {
|
|
21420
|
-
const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
22114
|
+
const mcpServers = isRecord$1(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
21421
22115
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
21422
22116
|
}
|
|
21423
|
-
const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
|
|
22117
|
+
const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
|
|
21424
22118
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
21425
22119
|
}
|
|
21426
22120
|
validate() {
|
|
@@ -21456,7 +22150,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
21456
22150
|
const result = {};
|
|
21457
22151
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21458
22152
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
21459
|
-
if (!isRecord(config)) continue;
|
|
22153
|
+
if (!isRecord$1(config)) continue;
|
|
21460
22154
|
const converted = {};
|
|
21461
22155
|
for (const [key, value] of Object.entries(config)) {
|
|
21462
22156
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -21471,7 +22165,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
21471
22165
|
function convertFromGrokFormat(grokMcp) {
|
|
21472
22166
|
const result = {};
|
|
21473
22167
|
for (const [name, config] of Object.entries(grokMcp)) {
|
|
21474
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22168
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21475
22169
|
const converted = {};
|
|
21476
22170
|
for (const [key, value] of Object.entries(config)) {
|
|
21477
22171
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -21609,7 +22303,7 @@ function resolveHermesTimeout(config) {
|
|
|
21609
22303
|
* import alike. See the Hermes mcp-config-reference.
|
|
21610
22304
|
*/
|
|
21611
22305
|
function copyHermesOauth(source) {
|
|
21612
|
-
if (!isRecord(source)) return;
|
|
22306
|
+
if (!isRecord$1(source)) return;
|
|
21613
22307
|
const oauth = {};
|
|
21614
22308
|
for (const key of [
|
|
21615
22309
|
"redirect_uri",
|
|
@@ -21743,13 +22437,13 @@ function convertServerToHermes(config) {
|
|
|
21743
22437
|
function convertToHermesFormat(mcpServers) {
|
|
21744
22438
|
const result = {};
|
|
21745
22439
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21746
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
22440
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
21747
22441
|
result[name] = convertServerToHermes(config);
|
|
21748
22442
|
}
|
|
21749
22443
|
return result;
|
|
21750
22444
|
}
|
|
21751
22445
|
function mergeHermesMcpServers(config, mcpServers) {
|
|
21752
|
-
const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
|
|
22446
|
+
const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
|
|
21753
22447
|
return {
|
|
21754
22448
|
...config,
|
|
21755
22449
|
mcp_servers: {
|
|
@@ -21768,7 +22462,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
21768
22462
|
const result = {};
|
|
21769
22463
|
const hermesOverrides = {};
|
|
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
|
const server = {};
|
|
21773
22467
|
if (typeof config.command === "string") server.command = config.command;
|
|
21774
22468
|
if (isStringArray$1(config.args)) server.args = config.args;
|
|
@@ -21777,7 +22471,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
21777
22471
|
if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
21778
22472
|
if (config.enabled === false) server.disabled = true;
|
|
21779
22473
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
21780
|
-
if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
22474
|
+
if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
21781
22475
|
result[name] = server;
|
|
21782
22476
|
const hermesServer = { ...server };
|
|
21783
22477
|
if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
|
|
@@ -21816,7 +22510,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
21816
22510
|
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
21817
22511
|
format: "yaml",
|
|
21818
22512
|
fileContent
|
|
21819
|
-
}), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
22513
|
+
}), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
21820
22514
|
this.config = merged;
|
|
21821
22515
|
super.setFileContent(applySharedConfigPatch({
|
|
21822
22516
|
fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
|
|
@@ -21880,7 +22574,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
21880
22574
|
});
|
|
21881
22575
|
}
|
|
21882
22576
|
toRulesyncMcp() {
|
|
21883
|
-
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
22577
|
+
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
21884
22578
|
return this.toRulesyncMcpDefault({
|
|
21885
22579
|
outputRoot: getHermesagentRulesyncOutputRoot({
|
|
21886
22580
|
nativeOutputRoot: this.outputRoot,
|
|
@@ -22192,7 +22886,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
|
|
|
22192
22886
|
*/
|
|
22193
22887
|
function readExistingKiloMcpEntries(fileContent) {
|
|
22194
22888
|
const mcp = parse(fileContent || "{}")?.mcp;
|
|
22195
|
-
if (!isRecord(mcp)) return {};
|
|
22889
|
+
if (!isRecord$1(mcp)) return {};
|
|
22196
22890
|
const entries = {};
|
|
22197
22891
|
for (const [serverName, entry] of Object.entries(mcp)) {
|
|
22198
22892
|
const result = KiloMcpServerSchema.safeParse(entry);
|
|
@@ -22496,7 +23190,7 @@ async function readKimiCodeConfig({ outputRoot }) {
|
|
|
22496
23190
|
return {
|
|
22497
23191
|
parsed: true,
|
|
22498
23192
|
content,
|
|
22499
|
-
mcp: isRecord(mcp) ? mcp : {}
|
|
23193
|
+
mcp: isRecord$1(mcp) ? mcp : {}
|
|
22500
23194
|
};
|
|
22501
23195
|
} catch {
|
|
22502
23196
|
return {
|
|
@@ -22650,7 +23344,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
22650
23344
|
static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
|
|
22651
23345
|
if (!global) return [];
|
|
22652
23346
|
const block = rulesyncMcp.getJson()["kimi-code"];
|
|
22653
|
-
if (!isRecord(block)) return [];
|
|
23347
|
+
if (!isRecord$1(block)) return [];
|
|
22654
23348
|
const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
|
|
22655
23349
|
const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
|
|
22656
23350
|
if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0) return [];
|
|
@@ -22703,6 +23397,68 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
22703
23397
|
};
|
|
22704
23398
|
//#endregion
|
|
22705
23399
|
//#region src/features/mcp/kiro-mcp.ts
|
|
23400
|
+
/**
|
|
23401
|
+
* Union of two optional string lists, preserving order and dropping duplicates.
|
|
23402
|
+
* Returns `undefined` only when neither side was authored at all, so the caller
|
|
23403
|
+
* omits the key entirely rather than writing an empty array — but an explicitly
|
|
23404
|
+
* authored `[]` is kept, which keeps import → generate idempotent.
|
|
23405
|
+
*/
|
|
23406
|
+
function mergeToolLists(...lists) {
|
|
23407
|
+
if (lists.every((list) => list === void 0)) return void 0;
|
|
23408
|
+
const merged = [];
|
|
23409
|
+
for (const list of lists) for (const tool of list ?? []) if (!merged.includes(tool)) merged.push(tool);
|
|
23410
|
+
return merged;
|
|
23411
|
+
}
|
|
23412
|
+
/**
|
|
23413
|
+
* Translate rulesync's Kiro-only authoring keys onto the field names Kiro
|
|
23414
|
+
* actually reads in `mcp.json`.
|
|
23415
|
+
*
|
|
23416
|
+
* - `kiroAutoApprove` → `autoApprove` (tools run without a confirmation prompt)
|
|
23417
|
+
* - `kiroAutoBlock` → `disabledTools` (tools hidden from the agent)
|
|
23418
|
+
*
|
|
23419
|
+
* `disabledTools` is the only block list Kiro reads, and it is also a canonical
|
|
23420
|
+
* rulesync field, so `kiroAutoBlock` is a redundant spelling of it. Prefer the
|
|
23421
|
+
* canonical field; see the note on `kiroAutoBlock` in `src/types/mcp.ts`.
|
|
23422
|
+
*
|
|
23423
|
+
* Both native names are documented per-server fields, so a config that already
|
|
23424
|
+
* spells them natively keeps working: the two lists are merged rather than
|
|
23425
|
+
* one overwriting the other.
|
|
23426
|
+
* @see https://kiro.dev/docs/mcp/configuration/
|
|
23427
|
+
*/
|
|
23428
|
+
function toKiroMcpServers(servers) {
|
|
23429
|
+
return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
|
|
23430
|
+
const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
|
|
23431
|
+
const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
|
|
23432
|
+
const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
|
|
23433
|
+
return [name, {
|
|
23434
|
+
...rest,
|
|
23435
|
+
...autoApprove !== void 0 && { autoApprove },
|
|
23436
|
+
...disabled !== void 0 && { disabledTools: disabled }
|
|
23437
|
+
}];
|
|
23438
|
+
}));
|
|
23439
|
+
}
|
|
23440
|
+
/**
|
|
23441
|
+
* Import direction of {@link toKiroMcpServers}: Kiro's `autoApprove` becomes the
|
|
23442
|
+
* rulesync-only `kiroAutoApprove` so a regenerate reproduces it. `disabledTools`
|
|
23443
|
+
* is left alone — it is already a canonical rulesync key with the same meaning,
|
|
23444
|
+
* so `kiroAutoBlock` deliberately has no import counterpart.
|
|
23445
|
+
*
|
|
23446
|
+
* Only a genuine string array is renamed. `kiroAutoApprove` is typed as one, so
|
|
23447
|
+
* moving a hand-written `"autoApprove": "all"` there would produce a
|
|
23448
|
+
* `.rulesync/mcp.jsonc` the next generate refuses to parse; such a value stays
|
|
23449
|
+
* under its original key and passes through untouched instead.
|
|
23450
|
+
*/
|
|
23451
|
+
function fromKiroMcpServers(servers) {
|
|
23452
|
+
return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
|
|
23453
|
+
if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
|
|
23454
|
+
const { autoApprove, ...rest } = server;
|
|
23455
|
+
if (!isStringArray$1(autoApprove)) return [name, server];
|
|
23456
|
+
return [name, {
|
|
23457
|
+
...rest,
|
|
23458
|
+
kiroAutoApprove: autoApprove
|
|
23459
|
+
}];
|
|
23460
|
+
}));
|
|
23461
|
+
}
|
|
22706
23462
|
var KiroMcp = class KiroMcp extends ToolMcp {
|
|
22707
23463
|
json;
|
|
22708
23464
|
constructor(params) {
|
|
@@ -22731,7 +23487,7 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
22731
23487
|
}
|
|
22732
23488
|
static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
|
|
22733
23489
|
const paths = this.getSettablePaths();
|
|
22734
|
-
const fileContent = JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2);
|
|
23490
|
+
const fileContent = JSON.stringify({ mcpServers: toKiroMcpServers(rulesyncMcp.getMcpServers()) }, null, 2);
|
|
22735
23491
|
return new KiroMcp({
|
|
22736
23492
|
outputRoot,
|
|
22737
23493
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -22741,7 +23497,9 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
22741
23497
|
});
|
|
22742
23498
|
}
|
|
22743
23499
|
toRulesyncMcp() {
|
|
22744
|
-
|
|
23500
|
+
const mcpServers = this.json.mcpServers;
|
|
23501
|
+
const translated = isMcpServers(mcpServers) ? fromKiroMcpServers(mcpServers) : {};
|
|
23502
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: translated }, null, 2) });
|
|
22745
23503
|
}
|
|
22746
23504
|
validate() {
|
|
22747
23505
|
return {
|
|
@@ -23627,7 +24385,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
|
|
|
23627
24385
|
});
|
|
23628
24386
|
}
|
|
23629
24387
|
function disabledNamesOf(config) {
|
|
23630
|
-
const mcpBlock = config && isRecord(config.mcp) ? config.mcp : {};
|
|
24388
|
+
const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
|
|
23631
24389
|
return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
23632
24390
|
}
|
|
23633
24391
|
/**
|
|
@@ -23746,7 +24504,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
23746
24504
|
const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
|
|
23747
24505
|
const disabledNames = managedNames.filter((name) => {
|
|
23748
24506
|
const server = servers[name];
|
|
23749
|
-
return isRecord(server) && server.disabled === true;
|
|
24507
|
+
return isRecord$1(server) && server.disabled === true;
|
|
23750
24508
|
});
|
|
23751
24509
|
const existingContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)) ?? "";
|
|
23752
24510
|
let existingParsed;
|
|
@@ -23760,7 +24518,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
23760
24518
|
logger?.warn(`Skipping the Rovo Dev mcp.disabledMcpServers update: ${formatError(error)}`);
|
|
23761
24519
|
return [];
|
|
23762
24520
|
}
|
|
23763
|
-
const existingMcp = isRecord(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
|
|
24521
|
+
const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
|
|
23764
24522
|
const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
|
|
23765
24523
|
const managedNameSet = new Set(managedNames);
|
|
23766
24524
|
const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
|
|
@@ -23952,7 +24710,7 @@ function deriveTransportAllowlist(servers) {
|
|
|
23952
24710
|
http: false
|
|
23953
24711
|
};
|
|
23954
24712
|
for (const server of Object.values(servers)) {
|
|
23955
|
-
if (!isRecord(server)) continue;
|
|
24713
|
+
if (!isRecord$1(server)) continue;
|
|
23956
24714
|
const transport = transportOf(server);
|
|
23957
24715
|
if (transport) allowlist[transport] = true;
|
|
23958
24716
|
}
|
|
@@ -27587,13 +28345,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
27587
28345
|
WebFetch: "webfetch",
|
|
27588
28346
|
Mcp: "mcp"
|
|
27589
28347
|
};
|
|
27590
|
-
const MCP_CANONICAL_PREFIX$
|
|
28348
|
+
const MCP_CANONICAL_PREFIX$2 = "mcp__";
|
|
27591
28349
|
/**
|
|
27592
28350
|
* Returns true if the canonical category is the per-tool MCP form
|
|
27593
28351
|
* `mcp__<server>__<tool>`.
|
|
27594
28352
|
*/
|
|
27595
28353
|
function isMcpScopedCategory(canonical) {
|
|
27596
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
28354
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$2) && canonical.length > 5;
|
|
27597
28355
|
}
|
|
27598
28356
|
function toCursorType(canonical) {
|
|
27599
28357
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -27627,7 +28385,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
27627
28385
|
if (match) {
|
|
27628
28386
|
const server = match[1] ?? "*";
|
|
27629
28387
|
const tool = match[2] ?? "*";
|
|
27630
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
28388
|
+
return `${MCP_CANONICAL_PREFIX$2}${server}__${tool}`;
|
|
27631
28389
|
}
|
|
27632
28390
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
27633
28391
|
}
|
|
@@ -27856,7 +28614,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
27856
28614
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
27857
28615
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
27858
28616
|
if (!permission[canonical]) permission[canonical] = {};
|
|
27859
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
28617
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$2) ? "*" : pattern;
|
|
27860
28618
|
permission[canonical][canonicalPattern] = action;
|
|
27861
28619
|
}
|
|
27862
28620
|
};
|
|
@@ -27991,14 +28749,14 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
27991
28749
|
let settings;
|
|
27992
28750
|
try {
|
|
27993
28751
|
const parsed = JSON.parse(existingContent);
|
|
27994
|
-
settings = isRecord(parsed) ? parsed : {};
|
|
28752
|
+
settings = isRecord$1(parsed) ? parsed : {};
|
|
27995
28753
|
} catch (error) {
|
|
27996
28754
|
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
27997
28755
|
}
|
|
27998
28756
|
const config = rulesyncPermissions.getJson();
|
|
27999
28757
|
const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
|
|
28000
28758
|
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
28001
|
-
const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
28759
|
+
const existingPermissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
28002
28760
|
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
28003
28761
|
const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
28004
28762
|
const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
|
|
@@ -28028,11 +28786,11 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
28028
28786
|
let settings;
|
|
28029
28787
|
try {
|
|
28030
28788
|
const parsed = JSON.parse(this.getFileContent());
|
|
28031
|
-
settings = isRecord(parsed) ? parsed : {};
|
|
28789
|
+
settings = isRecord$1(parsed) ? parsed : {};
|
|
28032
28790
|
} catch (error) {
|
|
28033
28791
|
throw new Error(`Failed to parse Devin permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28034
28792
|
}
|
|
28035
|
-
const permissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
28793
|
+
const permissions = isRecord$1(settings.permissions) ? settings.permissions : {};
|
|
28036
28794
|
const config = convertDevinToRulesyncPermissions({
|
|
28037
28795
|
allow: Array.isArray(permissions.allow) ? permissions.allow : [],
|
|
28038
28796
|
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
@@ -28119,7 +28877,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
|
|
|
28119
28877
|
"interactionMode",
|
|
28120
28878
|
"extraKnownMarketplaces",
|
|
28121
28879
|
"enabledPlugins",
|
|
28122
|
-
"hooksDisabled"
|
|
28880
|
+
"hooksDisabled",
|
|
28881
|
+
"disabledSkills"
|
|
28123
28882
|
];
|
|
28124
28883
|
/**
|
|
28125
28884
|
* Permissions adapter for Factory Droid.
|
|
@@ -28380,7 +29139,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
28380
29139
|
} catch (error) {
|
|
28381
29140
|
throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
28382
29141
|
}
|
|
28383
|
-
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
29142
|
+
const config = isRecord$1(parsed) ? { ...parsed } : {};
|
|
28384
29143
|
const userPermission = convertRulesyncToGoosePermissionConfig({
|
|
28385
29144
|
config: rulesyncPermissions.getJson(),
|
|
28386
29145
|
logger
|
|
@@ -28403,8 +29162,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
28403
29162
|
} catch (error) {
|
|
28404
29163
|
throw new Error(`Failed to parse Goose permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28405
29164
|
}
|
|
28406
|
-
const config = isRecord(parsed) ? parsed : {};
|
|
28407
|
-
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
29165
|
+
const config = isRecord$1(parsed) ? parsed : {};
|
|
29166
|
+
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
28408
29167
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
28409
29168
|
}
|
|
28410
29169
|
validate() {
|
|
@@ -28478,7 +29237,7 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
28478
29237
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
28479
29238
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
28480
29239
|
const CATCH_ALL_PATTERN$2 = "*";
|
|
28481
|
-
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
29240
|
+
const MCP_CANONICAL_PREFIX$1 = "mcp__";
|
|
28482
29241
|
const CATEGORY_TO_GROK_TOOL = {
|
|
28483
29242
|
bash: "Bash",
|
|
28484
29243
|
read: "Read",
|
|
@@ -28506,7 +29265,7 @@ const GROK_MCP_TOOL = "MCPTool";
|
|
|
28506
29265
|
* concrete pattern emits `Tool(pattern)`.
|
|
28507
29266
|
*/
|
|
28508
29267
|
function buildGrokEntry(category, pattern) {
|
|
28509
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
29268
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$1)) {
|
|
28510
29269
|
const remainder = category.slice(5);
|
|
28511
29270
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
28512
29271
|
}
|
|
@@ -28533,7 +29292,7 @@ function parseGrokEntry(entry) {
|
|
|
28533
29292
|
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
28534
29293
|
}
|
|
28535
29294
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
28536
|
-
category: `${MCP_CANONICAL_PREFIX}${inner}`,
|
|
29295
|
+
category: `${MCP_CANONICAL_PREFIX$1}${inner}`,
|
|
28537
29296
|
pattern: CATCH_ALL_PATTERN$2
|
|
28538
29297
|
} : {
|
|
28539
29298
|
category: "mcp",
|
|
@@ -28631,7 +29390,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
28631
29390
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
28632
29391
|
}
|
|
28633
29392
|
const config = rulesyncPermissions.getJson();
|
|
28634
|
-
const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
29393
|
+
const existingPermission = isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
28635
29394
|
const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
|
|
28636
29395
|
const permission = {
|
|
28637
29396
|
...existingPermission,
|
|
@@ -28640,7 +29399,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
28640
29399
|
ask: buckets.ask
|
|
28641
29400
|
};
|
|
28642
29401
|
const uiPatch = global ? { [GROKCLI_UI_KEY]: {
|
|
28643
|
-
...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
|
|
29402
|
+
...isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
|
|
28644
29403
|
[GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
|
|
28645
29404
|
} } : {};
|
|
28646
29405
|
return new GrokcliPermissions({
|
|
@@ -28669,7 +29428,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
28669
29428
|
} catch (error) {
|
|
28670
29429
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28671
29430
|
}
|
|
28672
|
-
const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
29431
|
+
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
28673
29432
|
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
|
|
28674
29433
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
28675
29434
|
}
|
|
@@ -28766,7 +29525,7 @@ function parseGrokPermissionArrays(permission) {
|
|
|
28766
29525
|
* `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
|
|
28767
29526
|
*/
|
|
28768
29527
|
function legacyModeAction(parsed) {
|
|
28769
|
-
return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
29528
|
+
return (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
28770
29529
|
}
|
|
28771
29530
|
/**
|
|
28772
29531
|
* Collapse a rulesync permissions config into Grok's single coarse mode.
|
|
@@ -28822,12 +29581,12 @@ function withoutKey(record, key) {
|
|
|
28822
29581
|
return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
|
|
28823
29582
|
}
|
|
28824
29583
|
function buildHermesOverride(config, provenance) {
|
|
28825
|
-
const base = isRecord(provenance.hermes) ? { ...provenance.hermes } : {};
|
|
28826
|
-
const approvalsOverride = withoutKey(isRecord(config.approvals) ? config.approvals : {}, "deny");
|
|
29584
|
+
const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
|
|
29585
|
+
const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
|
|
28827
29586
|
if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
|
|
28828
29587
|
else delete base.approvals;
|
|
28829
|
-
const security = isRecord(config.security) ? { ...config.security } : {};
|
|
28830
|
-
const blocklist = isRecord(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
|
|
29588
|
+
const security = isRecord$1(config.security) ? { ...config.security } : {};
|
|
29589
|
+
const blocklist = isRecord$1(security.website_blocklist) ? { ...security.website_blocklist } : void 0;
|
|
28831
29590
|
if (blocklist?.enabled === true) {
|
|
28832
29591
|
delete blocklist.domains;
|
|
28833
29592
|
delete blocklist.enabled;
|
|
@@ -28836,7 +29595,7 @@ function buildHermesOverride(config, provenance) {
|
|
|
28836
29595
|
else delete security.website_blocklist;
|
|
28837
29596
|
if (Object.keys(security).length > 0) base.security = security;
|
|
28838
29597
|
else delete base.security;
|
|
28839
|
-
for (const key of ["skills", "memory"]) if (isRecord(config[key])) base[key] = config[key];
|
|
29598
|
+
for (const key of ["skills", "memory"]) if (isRecord$1(config[key])) base[key] = config[key];
|
|
28840
29599
|
else delete base[key];
|
|
28841
29600
|
return base;
|
|
28842
29601
|
}
|
|
@@ -28908,7 +29667,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
28908
29667
|
format: "yaml",
|
|
28909
29668
|
fileContent: this.getFileContent()
|
|
28910
29669
|
});
|
|
28911
|
-
const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
|
|
29670
|
+
const permissionsRoot = isRecord$1(config.permissions) ? config.permissions : {};
|
|
28912
29671
|
const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(permissionsRoot.rulesync);
|
|
28913
29672
|
const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
|
|
28914
29673
|
const permission = clonePermissionBlock(provenance.permission);
|
|
@@ -28916,14 +29675,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
28916
29675
|
permission,
|
|
28917
29676
|
commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
|
|
28918
29677
|
});
|
|
28919
|
-
const approvals = isRecord(config.approvals) ? config.approvals : {};
|
|
29678
|
+
const approvals = isRecord$1(config.approvals) ? config.approvals : {};
|
|
28920
29679
|
reconcileNativeDenies({
|
|
28921
29680
|
permission,
|
|
28922
29681
|
category: "bash",
|
|
28923
29682
|
patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
|
|
28924
29683
|
});
|
|
28925
|
-
const security = isRecord(config.security) ? config.security : {};
|
|
28926
|
-
const websiteBlocklist = isRecord(security.website_blocklist) ? security.website_blocklist : {};
|
|
29684
|
+
const security = isRecord$1(config.security) ? config.security : {};
|
|
29685
|
+
const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
|
|
28927
29686
|
reconcileNativeDenies({
|
|
28928
29687
|
permission,
|
|
28929
29688
|
category: "webfetch",
|
|
@@ -29598,8 +30357,8 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
|
|
|
29598
30357
|
existing = void 0;
|
|
29599
30358
|
}
|
|
29600
30359
|
const merged = {
|
|
29601
|
-
...isRecord(existing) ? existing : {},
|
|
29602
|
-
...isRecord(patch.tools) ? patch.tools : {}
|
|
30360
|
+
...isRecord$1(existing) ? existing : {},
|
|
30361
|
+
...isRecord$1(patch.tools) ? patch.tools : {}
|
|
29603
30362
|
};
|
|
29604
30363
|
if (Object.keys(merged).length === 0) return;
|
|
29605
30364
|
warnAboutMistypedToolLists(merged);
|
|
@@ -29623,7 +30382,7 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
|
|
|
29623
30382
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#tools
|
|
29624
30383
|
*/
|
|
29625
30384
|
function buildKimiCodeToolsSection(tools) {
|
|
29626
|
-
if (!isRecord(tools)) return;
|
|
30385
|
+
if (!isRecord$1(tools)) return;
|
|
29627
30386
|
const section = Object.fromEntries(Object.entries(tools).filter(([key, value]) => key === "enabled" || key === "disabled" ? isStringList(value) : true));
|
|
29628
30387
|
return Object.keys(section).length > 0 ? section : void 0;
|
|
29629
30388
|
}
|
|
@@ -29722,7 +30481,7 @@ function preserveKimiCodeRules(rules) {
|
|
|
29722
30481
|
nativeRules
|
|
29723
30482
|
};
|
|
29724
30483
|
for (const raw of rules) {
|
|
29725
|
-
if (!isRecord(raw)) continue;
|
|
30484
|
+
if (!isRecord$1(raw)) continue;
|
|
29726
30485
|
const decision = raw.decision;
|
|
29727
30486
|
const pattern = raw.pattern;
|
|
29728
30487
|
if (decision !== "allow" && decision !== "ask" && decision !== "deny" || typeof pattern !== "string") continue;
|
|
@@ -29837,7 +30596,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
|
|
|
29837
30596
|
format: "toml",
|
|
29838
30597
|
fileContent: this.getFileContent()
|
|
29839
30598
|
});
|
|
29840
|
-
const { permission, nativeRules } = preserveKimiCodeRules((isRecord(config.permission) ? config.permission : {}).rules);
|
|
30599
|
+
const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
|
|
29841
30600
|
const defaultPermissionMode = config.default_permission_mode;
|
|
29842
30601
|
const tools = buildKimiCodeToolsSection(config.tools);
|
|
29843
30602
|
const toolOverride = {
|
|
@@ -31114,7 +31873,7 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
31114
31873
|
} catch (error) {
|
|
31115
31874
|
throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
31116
31875
|
}
|
|
31117
|
-
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
31876
|
+
const config = isRecord$1(parsed) ? { ...parsed } : {};
|
|
31118
31877
|
const rulesyncConfig = rulesyncPermissions.getJson();
|
|
31119
31878
|
const toolPermissions = convertRulesyncToRovodevToolPermissions({
|
|
31120
31879
|
config: rulesyncConfig,
|
|
@@ -31151,8 +31910,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
31151
31910
|
} catch (error) {
|
|
31152
31911
|
throw new Error(`Failed to parse Rovodev permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
31153
31912
|
}
|
|
31154
|
-
const config = isRecord(parsed) ? parsed : {};
|
|
31155
|
-
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
|
|
31913
|
+
const config = isRecord$1(parsed) ? parsed : {};
|
|
31914
|
+
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
|
|
31156
31915
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
31157
31916
|
}
|
|
31158
31917
|
validate() {
|
|
@@ -31179,14 +31938,14 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
31179
31938
|
* keys it does not are kept as-is.
|
|
31180
31939
|
*/
|
|
31181
31940
|
function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
|
|
31182
|
-
const existingToolPermissions = isRecord(existing) ? { ...existing } : {};
|
|
31941
|
+
const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
|
|
31183
31942
|
if (Object.keys(generated).length === 0 && sourceStatesRules) {
|
|
31184
|
-
if (!isRecord(existing)) return;
|
|
31943
|
+
if (!isRecord$1(existing)) return;
|
|
31185
31944
|
const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
|
|
31186
31945
|
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(", ")}.` : `.`));
|
|
31187
31946
|
return existingToolPermissions;
|
|
31188
31947
|
}
|
|
31189
|
-
const hasExistingToolsRecord = isRecord(existingToolPermissions.tools);
|
|
31948
|
+
const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
|
|
31190
31949
|
const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
|
|
31191
31950
|
warnAboutDroppedOwnedKeys({
|
|
31192
31951
|
existingToolPermissions,
|
|
@@ -31230,7 +31989,7 @@ function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, gen
|
|
|
31230
31989
|
*/
|
|
31231
31990
|
function stripPermissiveOwnedValues(toolPermissions) {
|
|
31232
31991
|
const strippedKeys = [];
|
|
31233
|
-
if (isRecord(toolPermissions.tools)) {
|
|
31992
|
+
if (isRecord$1(toolPermissions.tools)) {
|
|
31234
31993
|
const tools = { ...toolPermissions.tools };
|
|
31235
31994
|
for (const toolKey of MANAGED_TOOL_KEYS) if (tools[toolKey] === "allow") {
|
|
31236
31995
|
delete tools[toolKey];
|
|
@@ -31252,14 +32011,14 @@ function stripPermissiveOwnedValues(toolPermissions) {
|
|
|
31252
32011
|
strippedKeys.push("default");
|
|
31253
32012
|
}
|
|
31254
32013
|
const bash = toolPermissions.bash;
|
|
31255
|
-
if (isRecord(bash)) {
|
|
32014
|
+
if (isRecord$1(bash)) {
|
|
31256
32015
|
const stripped = { ...bash };
|
|
31257
32016
|
if (stripped.default === "allow") {
|
|
31258
32017
|
delete stripped.default;
|
|
31259
32018
|
strippedKeys.push("bash.default");
|
|
31260
32019
|
}
|
|
31261
32020
|
if (Array.isArray(stripped.commands)) {
|
|
31262
|
-
const kept = stripped.commands.filter((entry) => !(isRecord(entry) && entry.permission === "allow"));
|
|
32021
|
+
const kept = stripped.commands.filter((entry) => !(isRecord$1(entry) && entry.permission === "allow"));
|
|
31263
32022
|
if (kept.length !== stripped.commands.length) strippedKeys.push("bash.commands");
|
|
31264
32023
|
if (kept.length > 0) stripped.commands = kept;
|
|
31265
32024
|
else delete stripped.commands;
|
|
@@ -31367,15 +32126,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
|
|
|
31367
32126
|
const permission = {};
|
|
31368
32127
|
if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
|
|
31369
32128
|
const bash = toolPermissions.bash;
|
|
31370
|
-
if (isRecord(bash)) {
|
|
32129
|
+
if (isRecord$1(bash)) {
|
|
31371
32130
|
const bashRules = {};
|
|
31372
32131
|
if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
|
|
31373
32132
|
if (Array.isArray(bash.commands)) {
|
|
31374
|
-
for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
32133
|
+
for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
31375
32134
|
}
|
|
31376
32135
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
31377
32136
|
}
|
|
31378
|
-
const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
|
|
32137
|
+
const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
|
|
31379
32138
|
const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
|
|
31380
32139
|
for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
|
|
31381
32140
|
const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
|
|
@@ -32090,11 +32849,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32090
32849
|
config,
|
|
32091
32850
|
logger
|
|
32092
32851
|
});
|
|
32093
|
-
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
32094
|
-
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
32852
|
+
const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
|
|
32853
|
+
const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
|
|
32095
32854
|
const override = config.warp;
|
|
32096
|
-
const executionProfileOverride = isRecord(override) && isRecord(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
|
|
32097
|
-
if (isRecord(override)) {
|
|
32855
|
+
const executionProfileOverride = isRecord$1(override) && isRecord$1(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
|
|
32856
|
+
if (isRecord$1(override)) {
|
|
32098
32857
|
const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
|
|
32099
32858
|
Object.assign(profiles, legacyOverride);
|
|
32100
32859
|
}
|
|
@@ -32129,10 +32888,10 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32129
32888
|
} catch (error) {
|
|
32130
32889
|
throw new Error(`Failed to parse Warp permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
32131
32890
|
}
|
|
32132
|
-
const agents = isRecord(settings.agents) ? settings.agents : {};
|
|
32133
|
-
const profiles = isRecord(agents.profiles) ? agents.profiles : {};
|
|
32134
|
-
const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
|
|
32135
|
-
const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
|
|
32891
|
+
const agents = isRecord$1(settings.agents) ? settings.agents : {};
|
|
32892
|
+
const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
|
|
32893
|
+
const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
|
|
32894
|
+
const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
|
|
32136
32895
|
const config = convertWarpToRulesyncPermissions({
|
|
32137
32896
|
allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
32138
32897
|
deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
@@ -32179,12 +32938,12 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32179
32938
|
*/
|
|
32180
32939
|
function mergeIntoDefaultExecutionProfile({ agents, mergedAllow, mergedDeny, executionProfileOverride, logger }) {
|
|
32181
32940
|
const hasOverrideKeys = executionProfileOverride !== void 0 && Object.keys(executionProfileOverride).length > 0;
|
|
32182
|
-
if (!isRecord(agents[EXECUTION_PROFILES_KEY])) {
|
|
32941
|
+
if (!isRecord$1(agents[EXECUTION_PROFILES_KEY])) {
|
|
32183
32942
|
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.");
|
|
32184
32943
|
return;
|
|
32185
32944
|
}
|
|
32186
32945
|
const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
|
|
32187
|
-
const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
|
|
32946
|
+
const defaultProfile = isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
|
|
32188
32947
|
if (executionProfileOverride) Object.assign(defaultProfile, executionProfileOverride);
|
|
32189
32948
|
if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
|
|
32190
32949
|
else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
|
|
@@ -32258,9 +33017,13 @@ const ZedToolPermissionsSchema = z.looseObject({
|
|
|
32258
33017
|
default: z.optional(ZedPermissionActionSchema),
|
|
32259
33018
|
tools: z.optional(z.record(z.string(), ZedToolPermissionSchema))
|
|
32260
33019
|
});
|
|
33020
|
+
/** Canonical per-tool MCP category prefix: `mcp__<server>__<tool>`. */
|
|
33021
|
+
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
33022
|
+
/** Zed's per-tool MCP name prefix: `mcp:<server>:<tool>`. */
|
|
33023
|
+
const MCP_ZED_PREFIX = "mcp:";
|
|
32261
33024
|
/**
|
|
32262
33025
|
* Mapping from rulesync canonical tool category names to Zed agent tool names.
|
|
32263
|
-
* Unknown names are passed through as-is
|
|
33026
|
+
* Unknown names are passed through as-is.
|
|
32264
33027
|
*/
|
|
32265
33028
|
const CANONICAL_TO_ZED_TOOL_NAMES = {
|
|
32266
33029
|
bash: "terminal",
|
|
@@ -32270,13 +33033,93 @@ const CANONICAL_TO_ZED_TOOL_NAMES = {
|
|
|
32270
33033
|
webfetch: "fetch",
|
|
32271
33034
|
websearch: "search_web"
|
|
32272
33035
|
};
|
|
33036
|
+
/**
|
|
33037
|
+
* Canonical categories whose Zed tool is not permission-gated. Zed's gated list
|
|
33038
|
+
* is `terminal`, `edit_file`, `write_file`, `delete_path`, `move_path`,
|
|
33039
|
+
* `copy_path`, `create_directory`, `fetch`, `search_web` and `skill`; the
|
|
33040
|
+
* read-only tools (`read_file`, `grep`, `find_path`, `list_directory`) sit in
|
|
33041
|
+
* Zed's own `EXCLUDED_TOOLS` and never call `decide_permission_from_settings`,
|
|
33042
|
+
* so a `tools.<name>` entry for one is config Zed never consults. Zed's real
|
|
33043
|
+
* read-denial surface is `private_files`, which the ignore feature owns.
|
|
33044
|
+
*
|
|
33045
|
+
* @see https://zed.dev/docs/ai/tool-permissions#supported-tools
|
|
33046
|
+
*/
|
|
33047
|
+
const ZED_EXCLUDED_CANONICAL_CATEGORIES = /* @__PURE__ */ new Set([
|
|
33048
|
+
"read",
|
|
33049
|
+
"grep",
|
|
33050
|
+
"glob"
|
|
33051
|
+
]);
|
|
33052
|
+
/** The Zed-side spellings of the same tools, for a category that names one directly. */
|
|
33053
|
+
const ZED_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
33054
|
+
"read_file",
|
|
33055
|
+
"grep",
|
|
33056
|
+
"find_path",
|
|
33057
|
+
"list_directory"
|
|
33058
|
+
]);
|
|
33059
|
+
const isZedExcludedCategory = (category) => ZED_EXCLUDED_CANONICAL_CATEGORIES.has(category) || ZED_EXCLUDED_TOOL_NAMES.has(toZedToolName(category));
|
|
32273
33060
|
const ZED_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZED_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
33061
|
+
/**
|
|
33062
|
+
* Zed addresses an MCP tool as `mcp:<server>:<tool>`, where rulesync's canonical
|
|
33063
|
+
* category is `mcp__<server>__<tool>`. Without this translation the canonical
|
|
33064
|
+
* spelling lands under a key Zed never looks up.
|
|
33065
|
+
*
|
|
33066
|
+
* Only the FIRST separator is split, matching the `cursor-permissions.ts`
|
|
33067
|
+
* precedent: upstream's `mcp_tool_id` concatenates the two names without
|
|
33068
|
+
* escaping either, so a tool called `create__issue` is legitimately
|
|
33069
|
+
* `mcp:github:create__issue`. Splitting every separator would rewrite that into
|
|
33070
|
+
* a third key on the next generate.
|
|
33071
|
+
*
|
|
33072
|
+
* @see https://zed.dev/docs/ai/tool-permissions
|
|
33073
|
+
*/
|
|
32274
33074
|
function toZedToolName(canonical) {
|
|
33075
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
33076
|
+
const [server, ...toolParts] = canonical.slice(5).split("__");
|
|
33077
|
+
const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
|
|
33078
|
+
return `${MCP_ZED_PREFIX}${address}`;
|
|
33079
|
+
}
|
|
32275
33080
|
return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
|
|
32276
33081
|
}
|
|
32277
33082
|
function toCanonicalToolName(zedName) {
|
|
33083
|
+
if (zedName.startsWith(MCP_ZED_PREFIX)) {
|
|
33084
|
+
const [server, ...toolParts] = zedName.slice(4).split(":");
|
|
33085
|
+
const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
|
|
33086
|
+
return `${MCP_CANONICAL_PREFIX}${address}`;
|
|
33087
|
+
}
|
|
32278
33088
|
return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
|
|
32279
33089
|
}
|
|
33090
|
+
/**
|
|
33091
|
+
* Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
|
|
33092
|
+
* tool's text input, and it dispatches every MCP tool with a single empty input
|
|
33093
|
+
* (`&[String::new()]`, commented upstream as "MCP tools are gated only by tool
|
|
33094
|
+
* id (no per-input pattern matching)"). The regexes still run, but against `""`,
|
|
33095
|
+
* so a pattern-scoped rule silently does something other than what its author
|
|
33096
|
+
* meant — and a non-matching `always_allow` downgrades the outcome to confirm.
|
|
33097
|
+
* Only the category's `*` rule (Zed's per-tool `default`) is therefore emitted.
|
|
33098
|
+
*/
|
|
33099
|
+
const isMcpZedToolName = (zedName) => zedName.startsWith(MCP_ZED_PREFIX);
|
|
33100
|
+
/**
|
|
33101
|
+
* Zed looks a tool up by exact key on the full `mcp:<server>:<tool>` triple, so
|
|
33102
|
+
* an address is inert unless it names both a concrete server and a concrete
|
|
33103
|
+
* tool: a missing half matches nothing, and so does a wildcard, since Zed does
|
|
33104
|
+
* no glob or prefix matching on the key.
|
|
33105
|
+
*/
|
|
33106
|
+
function isInertMcpAddress(zedToolName) {
|
|
33107
|
+
if (!isMcpZedToolName(zedToolName)) return false;
|
|
33108
|
+
const [server, ...toolParts] = zedToolName.slice(4).split(":");
|
|
33109
|
+
const tool = toolParts.join(":");
|
|
33110
|
+
return !server || server === "*" || !tool || tool === "*";
|
|
33111
|
+
}
|
|
33112
|
+
/**
|
|
33113
|
+
* Strip pattern-scoped rules from an MCP category, warning once about the ones
|
|
33114
|
+
* dropped. Non-MCP categories are returned untouched.
|
|
33115
|
+
*/
|
|
33116
|
+
function withoutInertMcpPatterns({ category, zedToolName, rules, logger }) {
|
|
33117
|
+
if (!isMcpZedToolName(zedToolName)) return rules;
|
|
33118
|
+
const scopedPatterns = Object.keys(rules).filter((pattern) => pattern !== "*");
|
|
33119
|
+
if (scopedPatterns.length === 0) return rules;
|
|
33120
|
+
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.`);
|
|
33121
|
+
return Object.fromEntries(Object.entries(rules).filter(([pattern]) => pattern === "*"));
|
|
33122
|
+
}
|
|
32280
33123
|
const CANONICAL_TO_ZED_ACTION = {
|
|
32281
33124
|
allow: "allow",
|
|
32282
33125
|
ask: "confirm",
|
|
@@ -32319,6 +33162,53 @@ function buildZedToolPermission(rules) {
|
|
|
32319
33162
|
if (alwaysConfirm.length > 0) tool.always_confirm = alwaysConfirm;
|
|
32320
33163
|
return Object.keys(tool).length > 0 ? tool : null;
|
|
32321
33164
|
}
|
|
33165
|
+
/**
|
|
33166
|
+
* Split a canonical permission block into the Zed shapes it maps onto, plus the
|
|
33167
|
+
* categories the caller should report as dropped.
|
|
33168
|
+
*
|
|
33169
|
+
* The canonical `*` category is the all-tools catch-all. Zed's counterpart is
|
|
33170
|
+
* `agent.tool_permissions.default` (rung 6 of its precedence ladder), not a
|
|
33171
|
+
* `tools["*"]` entry — `*` is not a Zed tool name, so writing one produces a
|
|
33172
|
+
* rule Zed silently ignores. Only the category's own `*` pattern can be
|
|
33173
|
+
* expressed there: Zed's global default carries no pattern list, so
|
|
33174
|
+
* pattern-scoped rules in the `*` category are dropped with a warning instead of
|
|
33175
|
+
* being emitted as inert config.
|
|
33176
|
+
*/
|
|
33177
|
+
function buildZedToolPermissions({ permission, logger }) {
|
|
33178
|
+
let managedDefault;
|
|
33179
|
+
const managedTools = {};
|
|
33180
|
+
const excludedCategories = [];
|
|
33181
|
+
const inertMcpCategories = [];
|
|
33182
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
33183
|
+
if (category === "*") {
|
|
33184
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
33185
|
+
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.`);
|
|
33186
|
+
continue;
|
|
33187
|
+
}
|
|
33188
|
+
if (isZedExcludedCategory(category)) {
|
|
33189
|
+
if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
|
|
33190
|
+
continue;
|
|
33191
|
+
}
|
|
33192
|
+
const zedToolName = toZedToolName(category);
|
|
33193
|
+
if (isInertMcpAddress(zedToolName)) {
|
|
33194
|
+
inertMcpCategories.push(category);
|
|
33195
|
+
continue;
|
|
33196
|
+
}
|
|
33197
|
+
const tool = buildZedToolPermission(withoutInertMcpPatterns({
|
|
33198
|
+
category,
|
|
33199
|
+
zedToolName,
|
|
33200
|
+
rules,
|
|
33201
|
+
logger
|
|
33202
|
+
}));
|
|
33203
|
+
if (tool) managedTools[zedToolName] = tool;
|
|
33204
|
+
}
|
|
33205
|
+
return {
|
|
33206
|
+
managedDefault,
|
|
33207
|
+
managedTools,
|
|
33208
|
+
excludedCategories,
|
|
33209
|
+
inertMcpCategories
|
|
33210
|
+
};
|
|
33211
|
+
}
|
|
32322
33212
|
function asRecord(value) {
|
|
32323
33213
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
|
|
32324
33214
|
return Object.fromEntries(Object.entries(value));
|
|
@@ -32389,19 +33279,15 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
32389
33279
|
const agent = asRecord(settings.agent);
|
|
32390
33280
|
const toolPermissions = asRecord(agent.tool_permissions);
|
|
32391
33281
|
const existingTools = asRecord(toolPermissions.tools);
|
|
32392
|
-
|
|
32393
|
-
|
|
32394
|
-
|
|
32395
|
-
|
|
32396
|
-
|
|
32397
|
-
|
|
32398
|
-
continue;
|
|
32399
|
-
}
|
|
32400
|
-
const tool = buildZedToolPermission(rules);
|
|
32401
|
-
if (tool) managedTools[toZedToolName(category)] = tool;
|
|
32402
|
-
}
|
|
33282
|
+
const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
|
|
33283
|
+
permission: config.permission,
|
|
33284
|
+
logger
|
|
33285
|
+
});
|
|
33286
|
+
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\`.`);
|
|
33287
|
+
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.`);
|
|
32403
33288
|
const managedToolNames = new Set(Object.keys(managedTools));
|
|
32404
33289
|
if ("*" in config.permission) managedToolNames.add("*");
|
|
33290
|
+
for (const toolName of Object.keys(existingTools)) if (toolName.startsWith(MCP_CANONICAL_PREFIX)) managedToolNames.add(toolName);
|
|
32405
33291
|
const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
|
|
32406
33292
|
return new ZedPermissions({
|
|
32407
33293
|
outputRoot,
|
|
@@ -33191,18 +34077,16 @@ var DirFeatureProcessor = class {
|
|
|
33191
34077
|
})) dirHasChanges = true;
|
|
33192
34078
|
}
|
|
33193
34079
|
const otherFiles = aiDir.getOtherFiles();
|
|
33194
|
-
const otherFileContents = [];
|
|
33195
34080
|
for (const file of otherFiles) {
|
|
33196
|
-
|
|
33197
|
-
|
|
33198
|
-
|
|
33199
|
-
|
|
33200
|
-
|
|
33201
|
-
|
|
33202
|
-
|
|
33203
|
-
|
|
33204
|
-
|
|
33205
|
-
}
|
|
34081
|
+
if (dirHasChanges) break;
|
|
34082
|
+
const filePath = join(dirPath, file.relativeFilePathToDirPath);
|
|
34083
|
+
const existingBuffer = await readFileBufferOrNull(filePath);
|
|
34084
|
+
if (!companionFileContentsEquivalent({
|
|
34085
|
+
filePath,
|
|
34086
|
+
expected: file.fileBuffer,
|
|
34087
|
+
existing: existingBuffer,
|
|
34088
|
+
composed: file.composed
|
|
34089
|
+
})) dirHasChanges = true;
|
|
33206
34090
|
}
|
|
33207
34091
|
if (!dirHasChanges) continue;
|
|
33208
34092
|
const relativeDir = aiDir.getRelativePathFromCwd();
|
|
@@ -33222,11 +34106,8 @@ var DirFeatureProcessor = class {
|
|
|
33222
34106
|
await writeFileContent(join(dirPath, mainFile.name), mainFileContent);
|
|
33223
34107
|
changedPaths.push(join(relativeDir, mainFile.name));
|
|
33224
34108
|
}
|
|
33225
|
-
for (const
|
|
33226
|
-
|
|
33227
|
-
const content = otherFileContents[i];
|
|
33228
|
-
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.`);
|
|
33229
|
-
await writeFileContent(filePath, content);
|
|
34109
|
+
for (const file of otherFiles) {
|
|
34110
|
+
await writeFileBuffer(join(dirPath, file.relativeFilePathToDirPath), file.fileBuffer);
|
|
33230
34111
|
changedPaths.push(join(relativeDir, file.relativeFilePathToDirPath));
|
|
33231
34112
|
}
|
|
33232
34113
|
}
|
|
@@ -33952,6 +34833,39 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
33952
34833
|
return frontmatter;
|
|
33953
34834
|
}
|
|
33954
34835
|
/**
|
|
34836
|
+
* Escapes the glob metacharacters in a directory path so it matches literally.
|
|
34837
|
+
* A real directory name may contain them — `app/[slug]` in a Next.js tree is
|
|
34838
|
+
* the common case, and unescaped `[slug]` reads as a bracket expression that
|
|
34839
|
+
* matches a different subtree (or nothing at all).
|
|
34840
|
+
*
|
|
34841
|
+
* @see https://code.claude.com/docs/en/memory
|
|
34842
|
+
*/
|
|
34843
|
+
function escapeGlobLiteral(dirPath) {
|
|
34844
|
+
return dirPath.replaceAll(/[\\*?[\]{}()!]/g, "\\$&");
|
|
34845
|
+
}
|
|
34846
|
+
/**
|
|
34847
|
+
* Claude Code scopes a nested skill by its location: a skill living in
|
|
34848
|
+
* `apps/web/.claude/skills/deploy` only activates while working under
|
|
34849
|
+
* `apps/web`. rulesync generates every imported skill into the project-root
|
|
34850
|
+
* `.claude/skills/`, so on import that location-based scoping has to be
|
|
34851
|
+
* re-expressed as an explicit `paths` glob — otherwise the round-trip silently
|
|
34852
|
+
* promotes a subtree skill to global activation.
|
|
34853
|
+
*
|
|
34854
|
+
* Returns the derived glob for a nested discovery root, or `undefined` for the
|
|
34855
|
+
* project-root `.claude/skills` (and for any root whose subtree cannot be
|
|
34856
|
+
* determined), where no scoping is implied.
|
|
34857
|
+
*
|
|
34858
|
+
* @see https://code.claude.com/docs/en/skills
|
|
34859
|
+
*/
|
|
34860
|
+
function deriveNestedSkillPaths(relativeDirPath) {
|
|
34861
|
+
const posixDirPath = toPosixPath(relativeDirPath);
|
|
34862
|
+
const skillsDirSuffix = `/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`;
|
|
34863
|
+
if (!posixDirPath.endsWith(skillsDirSuffix)) return;
|
|
34864
|
+
const subtree = posixDirPath.slice(0, -skillsDirSuffix.length);
|
|
34865
|
+
if (subtree === "" || subtree === ".") return;
|
|
34866
|
+
return [`${escapeGlobLiteral(subtree)}/**`];
|
|
34867
|
+
}
|
|
34868
|
+
/**
|
|
33955
34869
|
* Represents a Claude Code skill directory.
|
|
33956
34870
|
* Unlike subagents and commands, skills are directories containing SKILL.md and other files.
|
|
33957
34871
|
* Extends ToolSkill to inherit directory management and security features from AiDir.
|
|
@@ -34039,6 +34953,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34039
34953
|
}
|
|
34040
34954
|
toRulesyncSkill() {
|
|
34041
34955
|
const frontmatter = this.getFrontmatter();
|
|
34956
|
+
const resolvedPaths = frontmatter.paths !== void 0 ? frontmatter.paths : deriveNestedSkillPaths(this.relativeDirPath);
|
|
34042
34957
|
const claudecodeSection = {
|
|
34043
34958
|
...frontmatter.when_to_use && { when_to_use: frontmatter.when_to_use },
|
|
34044
34959
|
...frontmatter["allowed-tools"] && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
@@ -34055,7 +34970,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34055
34970
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34056
34971
|
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34057
34972
|
...this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true },
|
|
34058
|
-
...
|
|
34973
|
+
...resolvedPaths !== void 0 && { paths: resolvedPaths }
|
|
34059
34974
|
};
|
|
34060
34975
|
const rulesyncFrontmatter = {
|
|
34061
34976
|
name: frontmatter.name,
|
|
@@ -34453,7 +35368,8 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
34453
35368
|
fileBuffer: Buffer.from(dump(openaiObject, {
|
|
34454
35369
|
lineWidth: -1,
|
|
34455
35370
|
noRefs: true
|
|
34456
|
-
}))
|
|
35371
|
+
})),
|
|
35372
|
+
composed: true
|
|
34457
35373
|
}] : baseOtherFiles;
|
|
34458
35374
|
return new CodexCliSkill({
|
|
34459
35375
|
outputRoot,
|
|
@@ -34513,7 +35429,11 @@ const CopilotSkillFrontmatterSchema = z.looseObject({
|
|
|
34513
35429
|
name: z.string(),
|
|
34514
35430
|
description: z.string(),
|
|
34515
35431
|
license: z.optional(z.string()),
|
|
34516
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
35432
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
35433
|
+
"argument-hint": z.optional(z.string()),
|
|
35434
|
+
"user-invocable": z.optional(z.boolean()),
|
|
35435
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
35436
|
+
context: z.optional(z.string())
|
|
34517
35437
|
});
|
|
34518
35438
|
/**
|
|
34519
35439
|
* Represents a GitHub Copilot skill directory.
|
|
@@ -34568,14 +35488,10 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34568
35488
|
};
|
|
34569
35489
|
}
|
|
34570
35490
|
toRulesyncSkill() {
|
|
34571
|
-
const
|
|
34572
|
-
const copilotSection = {
|
|
34573
|
-
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
34574
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
34575
|
-
};
|
|
35491
|
+
const { name, description, ...copilotSection } = this.getFrontmatter();
|
|
34576
35492
|
const rulesyncFrontmatter = {
|
|
34577
|
-
name
|
|
34578
|
-
description
|
|
35493
|
+
name,
|
|
35494
|
+
description,
|
|
34579
35495
|
targets: ["*"],
|
|
34580
35496
|
...Object.keys(copilotSection).length > 0 && { copilot: copilotSection }
|
|
34581
35497
|
};
|
|
@@ -34593,11 +35509,22 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34593
35509
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34594
35510
|
const settablePaths = CopilotSkill.getSettablePaths({ global });
|
|
34595
35511
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35512
|
+
const copilotSection = rulesyncFrontmatter.copilot;
|
|
35513
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
35514
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35515
|
+
section: copilotSection
|
|
35516
|
+
});
|
|
35517
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35518
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35519
|
+
section: copilotSection
|
|
35520
|
+
});
|
|
35521
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotFields } = copilotSection ?? {};
|
|
34596
35522
|
const copilotFrontmatter = {
|
|
35523
|
+
...copilotFields,
|
|
34597
35524
|
name: rulesyncFrontmatter.name,
|
|
34598
35525
|
description: rulesyncFrontmatter.description,
|
|
34599
|
-
...
|
|
34600
|
-
...
|
|
35526
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35527
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
34601
35528
|
};
|
|
34602
35529
|
return new CopilotSkill({
|
|
34603
35530
|
outputRoot,
|
|
@@ -34716,17 +35643,10 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
34716
35643
|
};
|
|
34717
35644
|
}
|
|
34718
35645
|
toRulesyncSkill() {
|
|
34719
|
-
const
|
|
34720
|
-
const copilotcliSection = {
|
|
34721
|
-
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
34722
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
34723
|
-
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
|
|
34724
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34725
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
|
|
34726
|
-
};
|
|
35646
|
+
const { name, description, ...copilotcliSection } = this.getFrontmatter();
|
|
34727
35647
|
const rulesyncFrontmatter = {
|
|
34728
|
-
name
|
|
34729
|
-
description
|
|
35648
|
+
name,
|
|
35649
|
+
description,
|
|
34730
35650
|
targets: ["*"],
|
|
34731
35651
|
...Object.keys(copilotcliSection).length > 0 && { copilotcli: copilotcliSection }
|
|
34732
35652
|
};
|
|
@@ -34744,14 +35664,22 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
34744
35664
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34745
35665
|
const settablePaths = CopilotcliSkill.getSettablePaths({ global });
|
|
34746
35666
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35667
|
+
const copilotcliSection = rulesyncFrontmatter.copilotcli;
|
|
35668
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
35669
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35670
|
+
section: copilotcliSection
|
|
35671
|
+
});
|
|
35672
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35673
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35674
|
+
section: copilotcliSection
|
|
35675
|
+
});
|
|
35676
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, ...copilotcliFields } = copilotcliSection ?? {};
|
|
34747
35677
|
const copilotcliFrontmatter = {
|
|
35678
|
+
...copilotcliFields,
|
|
34748
35679
|
name: rulesyncFrontmatter.name,
|
|
34749
35680
|
description: rulesyncFrontmatter.description,
|
|
34750
|
-
...
|
|
34751
|
-
...
|
|
34752
|
-
...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] },
|
|
34753
|
-
...rulesyncFrontmatter.copilotcli?.["user-invocable"] !== void 0 && { "user-invocable": rulesyncFrontmatter.copilotcli["user-invocable"] },
|
|
34754
|
-
...rulesyncFrontmatter.copilotcli?.["disable-model-invocation"] !== void 0 && { "disable-model-invocation": rulesyncFrontmatter.copilotcli["disable-model-invocation"] }
|
|
35681
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35682
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
34755
35683
|
};
|
|
34756
35684
|
return new CopilotcliSkill({
|
|
34757
35685
|
outputRoot,
|
|
@@ -35309,7 +36237,9 @@ const FactorydroidSkillFrontmatterSchema = z.looseObject({
|
|
|
35309
36237
|
name: z.string(),
|
|
35310
36238
|
description: z.string(),
|
|
35311
36239
|
"user-invocable": z.optional(z.boolean()),
|
|
35312
|
-
"disable-model-invocation": z.optional(z.boolean())
|
|
36240
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
36241
|
+
enabled: z.optional(z.boolean()),
|
|
36242
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
35313
36243
|
});
|
|
35314
36244
|
/**
|
|
35315
36245
|
* Represents a Factory Droid skill directory.
|
|
@@ -35365,7 +36295,9 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35365
36295
|
const frontmatter = this.getFrontmatter();
|
|
35366
36296
|
const factorydroidBlock = {
|
|
35367
36297
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
35368
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] }
|
|
36298
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
36299
|
+
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
36300
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
35369
36301
|
};
|
|
35370
36302
|
const rulesyncFrontmatter = {
|
|
35371
36303
|
name: frontmatter.name,
|
|
@@ -35387,19 +36319,22 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35387
36319
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
35388
36320
|
const settablePaths = FactorydroidSkill.getSettablePaths({ global });
|
|
35389
36321
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
36322
|
+
const factorydroidSection = rulesyncFrontmatter.factorydroid;
|
|
35390
36323
|
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35391
36324
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35392
|
-
section:
|
|
36325
|
+
section: factorydroidSection
|
|
35393
36326
|
});
|
|
35394
36327
|
const resolvedUserInvocable = resolveUserInvocable({
|
|
35395
36328
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35396
|
-
section:
|
|
36329
|
+
section: factorydroidSection
|
|
35397
36330
|
});
|
|
35398
36331
|
const factorydroidFrontmatter = {
|
|
35399
36332
|
name: rulesyncFrontmatter.name,
|
|
35400
36333
|
description: rulesyncFrontmatter.description,
|
|
35401
36334
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
35402
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
36335
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
36336
|
+
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
36337
|
+
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
35403
36338
|
};
|
|
35404
36339
|
return new FactorydroidSkill({
|
|
35405
36340
|
outputRoot,
|
|
@@ -36216,7 +37151,10 @@ var KimiCodeSkill = class KimiCodeSkill extends ToolSkill {
|
|
|
36216
37151
|
//#region src/features/skills/kiro-skill.ts
|
|
36217
37152
|
const KiroSkillFrontmatterSchema = z.looseObject({
|
|
36218
37153
|
name: z.string(),
|
|
36219
|
-
description: z.string()
|
|
37154
|
+
description: z.string(),
|
|
37155
|
+
license: z.optional(z.string()),
|
|
37156
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
37157
|
+
metadata: z.optional(z.looseObject({}))
|
|
36220
37158
|
});
|
|
36221
37159
|
/**
|
|
36222
37160
|
* Represents a Kiro skill directory.
|
|
@@ -36270,11 +37208,12 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
36270
37208
|
};
|
|
36271
37209
|
}
|
|
36272
37210
|
toRulesyncSkill() {
|
|
36273
|
-
const
|
|
37211
|
+
const { name, description, ...kiroSection } = this.getFrontmatter();
|
|
36274
37212
|
const rulesyncFrontmatter = {
|
|
36275
|
-
name
|
|
36276
|
-
description
|
|
36277
|
-
targets: ["*"]
|
|
37213
|
+
name,
|
|
37214
|
+
description,
|
|
37215
|
+
targets: ["*"],
|
|
37216
|
+
...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
|
|
36278
37217
|
};
|
|
36279
37218
|
return new RulesyncSkill({
|
|
36280
37219
|
outputRoot: this.outputRoot,
|
|
@@ -36291,6 +37230,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
36291
37230
|
const settablePaths = KiroSkill.getSettablePaths({ global });
|
|
36292
37231
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
36293
37232
|
const kiroFrontmatter = {
|
|
37233
|
+
...rulesyncFrontmatter.kiro,
|
|
36294
37234
|
name: rulesyncFrontmatter.name,
|
|
36295
37235
|
description: rulesyncFrontmatter.description
|
|
36296
37236
|
};
|
|
@@ -39420,7 +40360,17 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39420
40360
|
validate: true
|
|
39421
40361
|
});
|
|
39422
40362
|
}
|
|
39423
|
-
|
|
40363
|
+
/**
|
|
40364
|
+
* Last chance to adjust the tool frontmatter before it is written. The base
|
|
40365
|
+
* implementation only warns about names Claude Code rejects; plugin-scoped
|
|
40366
|
+
* subclasses extend it to drop fields Claude Code refuses to honor for
|
|
40367
|
+
* plugin-shipped agents.
|
|
40368
|
+
*/
|
|
40369
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
40370
|
+
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.`);
|
|
40371
|
+
return frontmatter;
|
|
40372
|
+
}
|
|
40373
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
39424
40374
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
39425
40375
|
const claudecodeSection = this.filterToolSpecificSection(rulesyncFrontmatter.claudecode ?? {}, ["name", "description"]);
|
|
39426
40376
|
const rawClaudecodeFrontmatter = {
|
|
@@ -39430,7 +40380,11 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39430
40380
|
};
|
|
39431
40381
|
const result = ClaudecodeSubagentFrontmatterSchema.safeParse(rawClaudecodeFrontmatter);
|
|
39432
40382
|
if (!result.success) throw new Error(`Invalid claudecode subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
39433
|
-
const claudecodeFrontmatter =
|
|
40383
|
+
const claudecodeFrontmatter = this.sanitizeFrontmatter({
|
|
40384
|
+
frontmatter: result.data,
|
|
40385
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
40386
|
+
logger
|
|
40387
|
+
});
|
|
39434
40388
|
const body = rulesyncSubagent.getBody();
|
|
39435
40389
|
const fileContent = stringifyFrontmatter(body, claudecodeFrontmatter);
|
|
39436
40390
|
const paths = this.getSettablePaths({ global });
|
|
@@ -39499,6 +40453,21 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39499
40453
|
};
|
|
39500
40454
|
//#endregion
|
|
39501
40455
|
//#region src/features/subagents/claudecode-plugin-subagent.ts
|
|
40456
|
+
/**
|
|
40457
|
+
* Claude Code refuses these for plugin-shipped agents "for security reasons",
|
|
40458
|
+
* so emitting them leaves the author believing the agent is constrained when it
|
|
40459
|
+
* is not. Only these three are dropped: the other fields upstream does not list
|
|
40460
|
+
* (e.g. `color`) are merely ignored, with no misleading security posture.
|
|
40461
|
+
*
|
|
40462
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
40463
|
+
*/
|
|
40464
|
+
const PLUGIN_FORBIDDEN_FIELDS = [
|
|
40465
|
+
"hooks",
|
|
40466
|
+
"mcpServers",
|
|
40467
|
+
"permissionMode"
|
|
40468
|
+
];
|
|
40469
|
+
/** The only `isolation` value plugin agents accept. */
|
|
40470
|
+
const PLUGIN_ISOLATION_VALUE = "worktree";
|
|
39502
40471
|
var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
39503
40472
|
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
39504
40473
|
const targets = rulesyncSubagent.getFrontmatter().targets;
|
|
@@ -39507,6 +40476,21 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
|
39507
40476
|
static getSettablePaths() {
|
|
39508
40477
|
return { relativeDirPath: CLAUDECODE_PLUGIN_AGENTS_DIR };
|
|
39509
40478
|
}
|
|
40479
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
40480
|
+
const sanitized = { ...super.sanitizeFrontmatter({
|
|
40481
|
+
frontmatter,
|
|
40482
|
+
relativeFilePath,
|
|
40483
|
+
logger
|
|
40484
|
+
}) };
|
|
40485
|
+
const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
|
|
40486
|
+
for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
|
|
40487
|
+
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.`);
|
|
40488
|
+
if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
|
|
40489
|
+
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.`);
|
|
40490
|
+
delete sanitized.isolation;
|
|
40491
|
+
}
|
|
40492
|
+
return sanitized;
|
|
40493
|
+
}
|
|
39510
40494
|
};
|
|
39511
40495
|
//#endregion
|
|
39512
40496
|
//#region src/features/subagents/cline-subagent.ts
|
|
@@ -42815,14 +43799,14 @@ var ZoocodeSubagent = class extends RooSubagent {
|
|
|
42815
43799
|
static toRooMode(rulesyncSubagent) {
|
|
42816
43800
|
const mode = super.toRooMode(rulesyncSubagent);
|
|
42817
43801
|
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
42818
|
-
const zoocodeSection = isRecord(frontmatter.zoocode) ? frontmatter.zoocode : {};
|
|
43802
|
+
const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
|
|
42819
43803
|
if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
|
|
42820
43804
|
return mode;
|
|
42821
43805
|
}
|
|
42822
43806
|
toRulesyncSubagents() {
|
|
42823
43807
|
return super.toRulesyncSubagents().map((subagent) => {
|
|
42824
43808
|
const frontmatter = subagent.getFrontmatter();
|
|
42825
|
-
const { allowedMcpServers, ...restRooSection } = isRecord(frontmatter.roo) ? { ...frontmatter.roo } : {};
|
|
43809
|
+
const { allowedMcpServers, ...restRooSection } = isRecord$1(frontmatter.roo) ? { ...frontmatter.roo } : {};
|
|
42826
43810
|
const rebuilt = {
|
|
42827
43811
|
...frontmatter,
|
|
42828
43812
|
targets: ["zoocode"],
|
|
@@ -43160,7 +44144,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
43160
44144
|
outputRoot: this.outputRoot,
|
|
43161
44145
|
relativeDirPath: RulesyncSubagent.getSettablePaths().relativeDirPath,
|
|
43162
44146
|
rulesyncSubagent,
|
|
43163
|
-
global: this.global
|
|
44147
|
+
global: this.global,
|
|
44148
|
+
logger: this.logger
|
|
43164
44149
|
}));
|
|
43165
44150
|
}
|
|
43166
44151
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
@@ -45113,7 +46098,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
|
45113
46098
|
};
|
|
45114
46099
|
//#endregion
|
|
45115
46100
|
//#region src/features/rules/copilot-rule.ts
|
|
45116
|
-
const CopilotRuleFrontmatterSchema = z.
|
|
46101
|
+
const CopilotRuleFrontmatterSchema = z.looseObject({
|
|
45117
46102
|
description: z.optional(z.string()),
|
|
45118
46103
|
applyTo: z.optional(z.string()),
|
|
45119
46104
|
name: z.optional(z.string()),
|
|
@@ -45169,15 +46154,13 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45169
46154
|
toRulesyncRule() {
|
|
45170
46155
|
let globs;
|
|
45171
46156
|
if (this.frontmatter.applyTo) globs = this.frontmatter.applyTo.split(",").map((g) => g.trim());
|
|
46157
|
+
const { description, applyTo: _applyTo, ...copilotFields } = this.frontmatter;
|
|
45172
46158
|
const rulesyncFrontmatter = {
|
|
45173
46159
|
targets: ["*"],
|
|
45174
46160
|
root: this.isRoot(),
|
|
45175
|
-
description
|
|
46161
|
+
description,
|
|
45176
46162
|
globs,
|
|
45177
|
-
...(
|
|
45178
|
-
...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
|
|
45179
|
-
...this.frontmatter.name && { name: this.frontmatter.name }
|
|
45180
|
-
} }
|
|
46163
|
+
...Object.keys(copilotFields).length > 0 && { copilot: copilotFields }
|
|
45181
46164
|
};
|
|
45182
46165
|
const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
|
|
45183
46166
|
return new RulesyncRule({
|
|
@@ -45194,10 +46177,9 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45194
46177
|
const root = rulesyncFrontmatter.root;
|
|
45195
46178
|
const paths = this.getSettablePaths({ global });
|
|
45196
46179
|
const copilotFrontmatter = {
|
|
46180
|
+
...rulesyncFrontmatter.copilot,
|
|
45197
46181
|
description: rulesyncFrontmatter.description,
|
|
45198
|
-
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45199
|
-
excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
|
|
45200
|
-
name: rulesyncFrontmatter.copilot?.name
|
|
46182
|
+
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45201
46183
|
};
|
|
45202
46184
|
const body = rulesyncRule.getBody();
|
|
45203
46185
|
if (root) return new CopilotRule({
|
|
@@ -50894,6 +51876,6 @@ async function importChecksCore(params) {
|
|
|
50894
51876
|
return writtenCount;
|
|
50895
51877
|
}
|
|
50896
51878
|
//#endregion
|
|
50897
|
-
export { JsonLogger as $,
|
|
51879
|
+
export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
|
|
50898
51880
|
|
|
50899
|
-
//# sourceMappingURL=import-
|
|
51881
|
+
//# sourceMappingURL=import-KXnvmbzr.js.map
|