rulesync 16.6.0 → 16.7.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 +42 -19
- package/dist/cli/index.js +42 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-D87qlRUj.cjs → import-XhKI9ZRY.cjs} +517 -92
- package/dist/{import-BlwypG9v.js → import-zyfw1Cq_.js} +513 -94
- package/dist/import-zyfw1Cq_.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +22 -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);
|
|
@@ -2157,6 +2168,7 @@ const HookDefinitionSchema = z.looseObject({
|
|
|
2157
2168
|
name: z.optional(safeString),
|
|
2158
2169
|
description: z.optional(safeString),
|
|
2159
2170
|
failClosed: z.optional(z.boolean()),
|
|
2171
|
+
commandRegex: z.optional(safeString),
|
|
2160
2172
|
sequential: z.optional(z.boolean()),
|
|
2161
2173
|
async: z.optional(z.boolean()),
|
|
2162
2174
|
env: z.optional(z.record(z.string(), safeString)),
|
|
@@ -2173,6 +2185,7 @@ const HookDefinitionSchema = z.looseObject({
|
|
|
2173
2185
|
metadata: z.optional(z.looseObject({})),
|
|
2174
2186
|
if: z.optional(safeString),
|
|
2175
2187
|
commandWindows: z.optional(safeString),
|
|
2188
|
+
additionalContextLimit: z.optional(z.int().check(nonnegative())),
|
|
2176
2189
|
asyncRewake: z.optional(z.boolean()),
|
|
2177
2190
|
continueOnBlock: z.optional(z.boolean())
|
|
2178
2191
|
});
|
|
@@ -4220,7 +4233,8 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
4220
4233
|
*/
|
|
4221
4234
|
const FactorydroidPermissionsOverrideSchema = z.looseObject({
|
|
4222
4235
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
4223
|
-
commandBlocklist: z.optional(z.array(z.string()))
|
|
4236
|
+
commandBlocklist: z.optional(z.array(z.string())),
|
|
4237
|
+
disabledSkills: z.optional(z.array(z.string()))
|
|
4224
4238
|
});
|
|
4225
4239
|
/**
|
|
4226
4240
|
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
@@ -4666,13 +4680,22 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
|
|
|
4666
4680
|
* `base_permission_profile` it is consumed by the profile builder, not
|
|
4667
4681
|
* written as a top-level config key.
|
|
4668
4682
|
*
|
|
4669
|
-
*
|
|
4670
|
-
*
|
|
4671
|
-
*
|
|
4672
|
-
*
|
|
4673
|
-
*
|
|
4674
|
-
*
|
|
4675
|
-
*
|
|
4683
|
+
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
4684
|
+
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
4685
|
+
* (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
|
|
4686
|
+
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
|
|
4687
|
+
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
4688
|
+
* `base_permission_profile` and `git_write_rules` are consumed by the profile
|
|
4689
|
+
* builder rather than written, as described above, and `permission` is the
|
|
4690
|
+
* tool-scoped canonical block, which `RulesyncPermissions.forTarget` strips out
|
|
4691
|
+
* of the override before it ever reaches the patch. The allowlist is what keeps
|
|
4692
|
+
* the override from clobbering a feature-owned key: `mcp_servers.*` per-MCP
|
|
4693
|
+
* gating is owned by the MCP feature (`codexcli-mcp.ts` already writes the
|
|
4694
|
+
* `mcp_servers` tables in the same `config.toml`), and `permissions` /
|
|
4695
|
+
* `default_permissions` are owned by the canonical model. The schema itself is
|
|
4696
|
+
* `looseObject` so an unmodeled key parses (and is then reported rather than
|
|
4697
|
+
* rejected outright); supporting a new top-level Codex config key means adding
|
|
4698
|
+
* it to `CODEXCLI_OVERRIDE_KEYS`.
|
|
4676
4699
|
*
|
|
4677
4700
|
* @see https://developers.openai.com/codex/config-reference
|
|
4678
4701
|
* @see https://developers.openai.com/codex/permissions
|
|
@@ -5147,7 +5170,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5147
5170
|
})),
|
|
5148
5171
|
copilot: z.optional(z.looseObject({
|
|
5149
5172
|
license: z.optional(z.string()),
|
|
5150
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
5173
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
5174
|
+
"argument-hint": z.optional(z.string()),
|
|
5175
|
+
"user-invocable": z.optional(z.boolean()),
|
|
5176
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
5177
|
+
context: z.optional(z.string())
|
|
5151
5178
|
})),
|
|
5152
5179
|
copilotcli: z.optional(z.looseObject({
|
|
5153
5180
|
license: z.optional(z.string()),
|
|
@@ -5205,7 +5232,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5205
5232
|
})),
|
|
5206
5233
|
factorydroid: z.optional(z.looseObject({
|
|
5207
5234
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
5208
|
-
"user-invocable": z.optional(z.boolean())
|
|
5235
|
+
"user-invocable": z.optional(z.boolean()),
|
|
5236
|
+
enabled: z.optional(z.boolean()),
|
|
5237
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
5209
5238
|
})),
|
|
5210
5239
|
grokcli: z.optional(z.looseObject({
|
|
5211
5240
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
@@ -5418,8 +5447,8 @@ async function getLocalSkillDirNames(outputRoot) {
|
|
|
5418
5447
|
* Resolve the effective `disable-model-invocation` value for a tool skill.
|
|
5419
5448
|
*
|
|
5420
5449
|
* 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
|
|
5450
|
+
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
5451
|
+
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5423
5452
|
* default with a per-target value. A defined section value (including `false`)
|
|
5424
5453
|
* always wins over the root default.
|
|
5425
5454
|
*
|
|
@@ -5432,8 +5461,8 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
5432
5461
|
* Resolve the effective `user-invocable` value for a tool skill.
|
|
5433
5462
|
*
|
|
5434
5463
|
* 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
|
|
5464
|
+
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
5465
|
+
* copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5437
5466
|
* per-target value. A defined section value (including `false`) always wins
|
|
5438
5467
|
* over the root default.
|
|
5439
5468
|
*
|
|
@@ -5646,6 +5675,31 @@ function fileContentsEquivalent({ filePath, expected, existing }) {
|
|
|
5646
5675
|
if (structured !== void 0) return structured;
|
|
5647
5676
|
return addTrailingNewline(expected) === addTrailingNewline(existing);
|
|
5648
5677
|
}
|
|
5678
|
+
/**
|
|
5679
|
+
* Whether an on-disk companion file is equivalent to the generated one.
|
|
5680
|
+
*
|
|
5681
|
+
* Companion files (everything beside a skill's `SKILL.md`) are written byte for
|
|
5682
|
+
* byte, so byte equality is the whole test for a user asset carried through
|
|
5683
|
+
* from the source directory: a CRLF fixture or a deliberately newline-less file
|
|
5684
|
+
* must compare equal to itself and unequal to a normalized copy, and a copy
|
|
5685
|
+
* that has drifted must be repaired rather than tolerated.
|
|
5686
|
+
*
|
|
5687
|
+
* A `composed` file is different — Rulesync builds it from frontmatter (Codex
|
|
5688
|
+
* CLI's `agents/openai.yaml`), so differing bytes fall back to the structured
|
|
5689
|
+
* comparison and a formatter re-indenting it is not reported as a change on
|
|
5690
|
+
* every generate. Only the structured verdict counts: there is deliberately no
|
|
5691
|
+
* text fallback, since trailing-whitespace-insensitive text equality is exactly
|
|
5692
|
+
* the normalization companion files no longer get.
|
|
5693
|
+
*/
|
|
5694
|
+
function companionFileContentsEquivalent({ filePath, expected, existing, composed = false }) {
|
|
5695
|
+
if (existing === null) return false;
|
|
5696
|
+
if (existing.equals(expected)) return true;
|
|
5697
|
+
if (!composed) return false;
|
|
5698
|
+
const expectedText = expected.toString("utf-8");
|
|
5699
|
+
const existingText = existing.toString("utf-8");
|
|
5700
|
+
if (!Buffer.from(expectedText, "utf-8").equals(expected) || !Buffer.from(existingText, "utf-8").equals(existing)) return false;
|
|
5701
|
+
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
5702
|
+
}
|
|
5649
5703
|
//#endregion
|
|
5650
5704
|
//#region src/types/feature-processor.ts
|
|
5651
5705
|
var FeatureProcessor = class {
|
|
@@ -7046,6 +7100,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7046
7100
|
ownedKeys: ["mcp", "tools"]
|
|
7047
7101
|
} }
|
|
7048
7102
|
},
|
|
7103
|
+
".config/goose/config.yaml": {
|
|
7104
|
+
format: "yaml",
|
|
7105
|
+
invalidRootPolicy: "error",
|
|
7106
|
+
features: { mcp: {
|
|
7107
|
+
kind: "replace-owned-keys",
|
|
7108
|
+
ownedKeys: ["extensions"]
|
|
7109
|
+
} }
|
|
7110
|
+
},
|
|
7049
7111
|
[CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
|
|
7050
7112
|
format: "toml",
|
|
7051
7113
|
features: {
|
|
@@ -9284,9 +9346,10 @@ const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
|
9284
9346
|
//#region src/features/commands/factorydroid-command.ts
|
|
9285
9347
|
const FactorydroidCommandFrontmatterSchema = z.looseObject({
|
|
9286
9348
|
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())]))
|
|
9349
|
+
"argument-hint": z.optional(z.string())
|
|
9289
9350
|
});
|
|
9351
|
+
/** Not a Droid command surface; see the schema comment above. */
|
|
9352
|
+
const FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS = ["allowed-tools"];
|
|
9290
9353
|
var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
9291
9354
|
frontmatter;
|
|
9292
9355
|
body;
|
|
@@ -9313,6 +9376,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9313
9376
|
}
|
|
9314
9377
|
toRulesyncCommand() {
|
|
9315
9378
|
const { description, ...restFields } = this.frontmatter;
|
|
9379
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete restFields[field];
|
|
9316
9380
|
const rulesyncFrontmatter = {
|
|
9317
9381
|
targets: ["*"],
|
|
9318
9382
|
description,
|
|
@@ -9336,6 +9400,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9336
9400
|
description: rulesyncFrontmatter.description,
|
|
9337
9401
|
...factorydroidFields
|
|
9338
9402
|
};
|
|
9403
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete factorydroidFrontmatter[field];
|
|
9339
9404
|
const body = rulesyncCommand.getBody();
|
|
9340
9405
|
const paths = this.getSettablePaths({ global });
|
|
9341
9406
|
return new FactorydroidCommand({
|
|
@@ -13047,16 +13112,28 @@ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
|
|
|
13047
13112
|
};
|
|
13048
13113
|
}
|
|
13049
13114
|
/**
|
|
13050
|
-
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13051
|
-
* preserving insertion order of both keys and grouped definitions.
|
|
13115
|
+
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13116
|
+
* absent), preserving insertion order of both keys and grouped definitions.
|
|
13117
|
+
* Definitions that disagree on a `subdividesGroup` passthrough field are split
|
|
13118
|
+
* into separate groups, so a restricting field is never inherited by a hook
|
|
13119
|
+
* that did not ask for it.
|
|
13052
13120
|
*/
|
|
13053
|
-
function groupDefinitionsByMatcher(definitions) {
|
|
13121
|
+
function groupDefinitionsByMatcher({ definitions, converterConfig }) {
|
|
13122
|
+
const subdividingFields = (converterConfig.groupPassthroughFields ?? []).filter(({ subdividesGroup }) => subdividesGroup);
|
|
13054
13123
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
13055
13124
|
for (const def of definitions) {
|
|
13056
|
-
const
|
|
13057
|
-
const
|
|
13058
|
-
|
|
13059
|
-
|
|
13125
|
+
const rawMatcher = def.matcher ?? "";
|
|
13126
|
+
const matcher = converterConfig.wildcardMatcherMeansAll && rawMatcher === "*" ? "" : rawMatcher;
|
|
13127
|
+
const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
|
|
13128
|
+
const value = def[canonical];
|
|
13129
|
+
return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
13130
|
+
})].join("\0");
|
|
13131
|
+
const group = byMatcher.get(key);
|
|
13132
|
+
if (group) group.defs.push(def);
|
|
13133
|
+
else byMatcher.set(key, {
|
|
13134
|
+
matcher,
|
|
13135
|
+
defs: [def]
|
|
13136
|
+
});
|
|
13060
13137
|
}
|
|
13061
13138
|
return byMatcher;
|
|
13062
13139
|
}
|
|
@@ -13101,6 +13178,24 @@ function importBooleanPassthroughFields({ h, converterConfig }) {
|
|
|
13101
13178
|
return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "boolean").map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13102
13179
|
}
|
|
13103
13180
|
/**
|
|
13181
|
+
* Emit the configured number passthrough fields on the tool side, mapping each
|
|
13182
|
+
* canonical field name to its (possibly renamed) tool field name. Only finite
|
|
13183
|
+
* numbers are carried through.
|
|
13184
|
+
*/
|
|
13185
|
+
function emitNumberPassthroughFields({ def, hookType, converterConfig }) {
|
|
13186
|
+
return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13187
|
+
if (commandOnly === true && hookType !== "command") return false;
|
|
13188
|
+
return Number.isFinite(def[canonical]);
|
|
13189
|
+
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13190
|
+
}
|
|
13191
|
+
/**
|
|
13192
|
+
* Import the configured number passthrough fields back into canonical fields,
|
|
13193
|
+
* reversing {@link emitNumberPassthroughFields}. Only finite numbers are read.
|
|
13194
|
+
*/
|
|
13195
|
+
function importNumberPassthroughFields({ h, converterConfig }) {
|
|
13196
|
+
return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ tool }) => Number.isFinite(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13197
|
+
}
|
|
13198
|
+
/**
|
|
13104
13199
|
* Emit the configured string passthrough fields on the tool side, mapping each
|
|
13105
13200
|
* canonical field name to its (possibly renamed) tool field name. Only non-empty
|
|
13106
13201
|
* string values are carried through.
|
|
@@ -13138,17 +13233,45 @@ function importArrayPassthroughFields({ h, converterConfig, logger }) {
|
|
|
13138
13233
|
return Object.fromEntries(fields.filter(({ tool }) => isSafeStringArray(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13139
13234
|
}
|
|
13140
13235
|
/**
|
|
13236
|
+
* Emit the configured string-map passthrough fields on the tool side. Only maps
|
|
13237
|
+
* whose values are all control-character-free strings are carried through.
|
|
13238
|
+
*/
|
|
13239
|
+
function emitRecordPassthroughFields({ def, hookType, converterConfig }) {
|
|
13240
|
+
return Object.fromEntries((converterConfig.recordPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13241
|
+
if (commandOnly === true && hookType !== "command") return false;
|
|
13242
|
+
return isSafeStringRecord(def[canonical]);
|
|
13243
|
+
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13244
|
+
}
|
|
13245
|
+
/**
|
|
13246
|
+
* Import the configured string-map passthrough fields, reversing
|
|
13247
|
+
* {@link emitRecordPassthroughFields}.
|
|
13248
|
+
*/
|
|
13249
|
+
function importRecordPassthroughFields({ h, hookType, converterConfig, logger }) {
|
|
13250
|
+
const fields = (converterConfig.recordPassthroughFields ?? []).filter(({ commandOnly }) => commandOnly !== true || hookType === "command");
|
|
13251
|
+
for (const { tool } of fields) if (h[tool] !== void 0 && !isSafeStringRecord(h[tool])) logger?.warn(`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.`);
|
|
13252
|
+
return Object.fromEntries(fields.filter(({ tool }) => isSafeStringRecord(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13253
|
+
}
|
|
13254
|
+
/**
|
|
13255
|
+
* Check a value against the shape its field documents. A string field also
|
|
13256
|
+
* rejects control characters, matching the canonical `safeString` so an
|
|
13257
|
+
* imported value cannot fail validation on the next generate.
|
|
13258
|
+
*/
|
|
13259
|
+
function isGroupPassthroughValue(value, valueType = "object") {
|
|
13260
|
+
if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
|
|
13261
|
+
return isPlainObject$1(value);
|
|
13262
|
+
}
|
|
13263
|
+
/**
|
|
13141
13264
|
* Emit the configured group-level passthrough fields, taken from the first
|
|
13142
13265
|
* definition of the group that carries one.
|
|
13143
13266
|
*/
|
|
13144
13267
|
function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
|
|
13145
13268
|
const emitted = {};
|
|
13146
|
-
for (const { canonical, tool } of converterConfig.groupPassthroughFields ?? []) {
|
|
13269
|
+
for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
|
|
13147
13270
|
const carried = defs.map((def) => def[canonical]);
|
|
13148
|
-
const first = carried.find((value) =>
|
|
13271
|
+
const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
|
|
13149
13272
|
if (first === void 0) continue;
|
|
13150
13273
|
const firstStable = stableJson(first);
|
|
13151
|
-
const agrees = (value) =>
|
|
13274
|
+
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
13152
13275
|
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
13276
|
emitted[tool] = first;
|
|
13154
13277
|
}
|
|
@@ -13160,7 +13283,7 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
13160
13283
|
*/
|
|
13161
13284
|
function importGroupPassthroughFields({ rawEntry, converterConfig }) {
|
|
13162
13285
|
const entry = rawEntry;
|
|
13163
|
-
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool }) =>
|
|
13286
|
+
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
13164
13287
|
}
|
|
13165
13288
|
/**
|
|
13166
13289
|
* Emit the payload fields specific to a hook type — `url`/`headers`/
|
|
@@ -13206,6 +13329,11 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13206
13329
|
hookType,
|
|
13207
13330
|
converterConfig
|
|
13208
13331
|
}),
|
|
13332
|
+
...emitNumberPassthroughFields({
|
|
13333
|
+
def,
|
|
13334
|
+
hookType,
|
|
13335
|
+
converterConfig
|
|
13336
|
+
}),
|
|
13209
13337
|
...emitStringPassthroughFields({
|
|
13210
13338
|
def,
|
|
13211
13339
|
hookType,
|
|
@@ -13216,6 +13344,11 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13216
13344
|
hookType,
|
|
13217
13345
|
converterConfig
|
|
13218
13346
|
}),
|
|
13347
|
+
...emitRecordPassthroughFields({
|
|
13348
|
+
def,
|
|
13349
|
+
hookType,
|
|
13350
|
+
converterConfig
|
|
13351
|
+
}),
|
|
13219
13352
|
type: hookType,
|
|
13220
13353
|
...command !== void 0 && command !== null && { command },
|
|
13221
13354
|
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
@@ -13246,10 +13379,13 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
13246
13379
|
const result = {};
|
|
13247
13380
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
13248
13381
|
const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
|
|
13249
|
-
const byMatcher = groupDefinitionsByMatcher(
|
|
13382
|
+
const byMatcher = groupDefinitionsByMatcher({
|
|
13383
|
+
definitions,
|
|
13384
|
+
converterConfig
|
|
13385
|
+
});
|
|
13250
13386
|
const entries = [];
|
|
13251
13387
|
const isNoMatcherEvent = converterConfig.noMatcherEvents?.has(eventName) ?? false;
|
|
13252
|
-
for (const
|
|
13388
|
+
for (const { matcher: matcherKey, defs } of byMatcher.values()) {
|
|
13253
13389
|
if (isNoMatcherEvent && matcherKey) logger?.warn(`matcher "${matcherKey}" on "${eventName}" hook will be ignored — this event does not support matchers`);
|
|
13254
13390
|
const hooks = buildToolHooks({
|
|
13255
13391
|
defs,
|
|
@@ -13329,9 +13465,27 @@ function isStringArray(value) {
|
|
|
13329
13465
|
}
|
|
13330
13466
|
/** Compare object values without letting key order decide the answer. */
|
|
13331
13467
|
function stableJson(value) {
|
|
13468
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
13332
13469
|
return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
|
|
13333
13470
|
}
|
|
13334
13471
|
/**
|
|
13472
|
+
* A string map safe to hand a tool as a hook's environment block. On top of
|
|
13473
|
+
* {@link isStringRecord} it rejects a non-plain object (a class instance is not
|
|
13474
|
+
* data) and applies the control-character rule to the values, as
|
|
13475
|
+
* {@link isSafeStringArray} does for `args`.
|
|
13476
|
+
*
|
|
13477
|
+
* The keys are checked more strictly than the values. A tool builds each entry
|
|
13478
|
+
* back into a `KEY=VALUE` string for the spawned process, so a key holding `=`
|
|
13479
|
+
* (or a control character, or nothing at all) names a different variable than
|
|
13480
|
+
* it appears to — `PATH=/tmp/evil` written as a key would set `PATH`. An
|
|
13481
|
+
* authored `.rulesync/hooks.*` can arrive via `rulesync fetch`, so that is not
|
|
13482
|
+
* a shape to pass along.
|
|
13483
|
+
*/
|
|
13484
|
+
function isSafeStringRecord(value) {
|
|
13485
|
+
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
13486
|
+
return Object.entries(value).every(([key, entry]) => key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || entry.includes(char)));
|
|
13487
|
+
}
|
|
13488
|
+
/**
|
|
13335
13489
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
13336
13490
|
* field the schema guards with `safeString`, or the next generate fails
|
|
13337
13491
|
* validation on a file this import itself wrote — and the hooks feature is
|
|
@@ -13384,6 +13538,10 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
|
|
|
13384
13538
|
h,
|
|
13385
13539
|
converterConfig
|
|
13386
13540
|
}),
|
|
13541
|
+
...importNumberPassthroughFields({
|
|
13542
|
+
h,
|
|
13543
|
+
converterConfig
|
|
13544
|
+
}),
|
|
13387
13545
|
...importStringPassthroughFields({
|
|
13388
13546
|
h,
|
|
13389
13547
|
converterConfig
|
|
@@ -13393,6 +13551,12 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
|
|
|
13393
13551
|
converterConfig,
|
|
13394
13552
|
logger
|
|
13395
13553
|
}),
|
|
13554
|
+
...importRecordPassthroughFields({
|
|
13555
|
+
h,
|
|
13556
|
+
hookType,
|
|
13557
|
+
converterConfig,
|
|
13558
|
+
logger
|
|
13559
|
+
}),
|
|
13396
13560
|
...importGroupPassthroughFields({
|
|
13397
13561
|
rawEntry,
|
|
13398
13562
|
converterConfig
|
|
@@ -13910,6 +14074,14 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13910
14074
|
isDeletable() {
|
|
13911
14075
|
return false;
|
|
13912
14076
|
}
|
|
14077
|
+
/**
|
|
14078
|
+
* The converter config used for both directions. Exposed as a static hook so
|
|
14079
|
+
* plugin-scoped subclasses can swap tool-specific details (e.g. the project
|
|
14080
|
+
* directory variable) without duplicating the rest of the config.
|
|
14081
|
+
*/
|
|
14082
|
+
static getConverterConfig() {
|
|
14083
|
+
return CLAUDE_CONVERTER_CONFIG;
|
|
14084
|
+
}
|
|
13913
14085
|
static getSettablePaths(_options = {}) {
|
|
13914
14086
|
return {
|
|
13915
14087
|
relativeDirPath: CLAUDECODE_DIR,
|
|
@@ -13935,7 +14107,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13935
14107
|
const claudeHooks = canonicalToToolHooks({
|
|
13936
14108
|
config,
|
|
13937
14109
|
toolOverrideHooks: config.claudecode?.hooks,
|
|
13938
|
-
converterConfig:
|
|
14110
|
+
converterConfig: this.getConverterConfig(),
|
|
13939
14111
|
logger
|
|
13940
14112
|
});
|
|
13941
14113
|
const fileContent = applySharedConfigPatch({
|
|
@@ -13962,7 +14134,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13962
14134
|
}
|
|
13963
14135
|
const hooks = toolHooksToCanonical({
|
|
13964
14136
|
hooks: settings.hooks,
|
|
13965
|
-
converterConfig:
|
|
14137
|
+
converterConfig: this.constructor.getConverterConfig()
|
|
13966
14138
|
});
|
|
13967
14139
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
13968
14140
|
hooks,
|
|
@@ -13991,6 +14163,21 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
|
13991
14163
|
isDeletable() {
|
|
13992
14164
|
return true;
|
|
13993
14165
|
}
|
|
14166
|
+
/**
|
|
14167
|
+
* Plugin hook scripts ship inside the plugin, so their commands must resolve
|
|
14168
|
+
* against the plugin install directory rather than the consumer's project
|
|
14169
|
+
* root. Upstream documents `"${CLAUDE_PLUGIN_ROOT}"/scripts/format-code.sh`;
|
|
14170
|
+
* `$CLAUDE_PROJECT_DIR` would expand to a path in the consumer's own repo,
|
|
14171
|
+
* where the bundled script does not exist.
|
|
14172
|
+
*
|
|
14173
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
14174
|
+
*/
|
|
14175
|
+
static getConverterConfig() {
|
|
14176
|
+
return {
|
|
14177
|
+
...super.getConverterConfig(),
|
|
14178
|
+
projectDirVar: "$CLAUDE_PLUGIN_ROOT"
|
|
14179
|
+
};
|
|
14180
|
+
}
|
|
13994
14181
|
static getSettablePaths() {
|
|
13995
14182
|
return {
|
|
13996
14183
|
relativeDirPath: CLAUDECODE_PLUGIN_HOOKS_DIR,
|
|
@@ -14013,6 +14200,10 @@ const CODEXCLI_CONVERTER_CONFIG = {
|
|
|
14013
14200
|
}, {
|
|
14014
14201
|
canonical: "statusMessage",
|
|
14015
14202
|
tool: "statusMessage"
|
|
14203
|
+
}],
|
|
14204
|
+
numberPassthroughFields: [{
|
|
14205
|
+
canonical: "additionalContextLimit",
|
|
14206
|
+
tool: "additionalContextLimit"
|
|
14016
14207
|
}]
|
|
14017
14208
|
};
|
|
14018
14209
|
/**
|
|
@@ -15056,7 +15247,13 @@ const FACTORYDROID_CONVERTER_CONFIG = {
|
|
|
15056
15247
|
toolToCanonicalEventNames: FACTORYDROID_TO_CANONICAL_EVENT_NAMES,
|
|
15057
15248
|
projectDirVar: "$FACTORY_PROJECT_DIR",
|
|
15058
15249
|
prefixDotRelativeCommandsOnly: true,
|
|
15059
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15250
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15251
|
+
groupPassthroughFields: [{
|
|
15252
|
+
canonical: "commandRegex",
|
|
15253
|
+
tool: "commandRegex",
|
|
15254
|
+
valueType: "string",
|
|
15255
|
+
subdividesGroup: true
|
|
15256
|
+
}]
|
|
15060
15257
|
};
|
|
15061
15258
|
var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
15062
15259
|
constructor(params) {
|
|
@@ -15152,7 +15349,8 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
15152
15349
|
canonicalToToolEventNames: CANONICAL_TO_GOOSE_EVENT_NAMES,
|
|
15153
15350
|
toolToCanonicalEventNames: GOOSE_TO_CANONICAL_EVENT_NAMES,
|
|
15154
15351
|
projectDirVar: "",
|
|
15155
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15352
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15353
|
+
wildcardMatcherMeansAll: true
|
|
15156
15354
|
};
|
|
15157
15355
|
/**
|
|
15158
15356
|
* Represents a Goose lifecycle hooks file.
|
|
@@ -15247,6 +15445,11 @@ const GROKCLI_CONVERTER_CONFIG = {
|
|
|
15247
15445
|
toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
|
|
15248
15446
|
projectDirVar: "",
|
|
15249
15447
|
supportedHookTypes: /* @__PURE__ */ new Set(["command", "http"]),
|
|
15448
|
+
recordPassthroughFields: [{
|
|
15449
|
+
canonical: "env",
|
|
15450
|
+
tool: "env",
|
|
15451
|
+
commandOnly: true
|
|
15452
|
+
}],
|
|
15250
15453
|
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
15251
15454
|
"sessionStart",
|
|
15252
15455
|
"sessionEnd",
|
|
@@ -21197,6 +21400,27 @@ function resolveGooseType(config, url) {
|
|
|
21197
21400
|
return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
|
|
21198
21401
|
}
|
|
21199
21402
|
/**
|
|
21403
|
+
* The Goose extension types that carry an MCP server. Goose also documents
|
|
21404
|
+
* `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
|
|
21405
|
+
* no canonical MCP counterpart: they name capabilities Goose provides itself
|
|
21406
|
+
* rather than a server rulesync could describe.
|
|
21407
|
+
*/
|
|
21408
|
+
const GOOSE_MCP_EXTENSION_TYPES = /* @__PURE__ */ new Set([
|
|
21409
|
+
"stdio",
|
|
21410
|
+
"streamable_http",
|
|
21411
|
+
"sse"
|
|
21412
|
+
]);
|
|
21413
|
+
/**
|
|
21414
|
+
* Resolves the Goose extension type of an existing `extensions:` entry the way
|
|
21415
|
+
* Goose itself reads it: the declared `type`, or the shape of the entry when
|
|
21416
|
+
* the key is absent.
|
|
21417
|
+
*/
|
|
21418
|
+
function existingExtensionType(ext) {
|
|
21419
|
+
if (typeof ext.type === "string") return ext.type;
|
|
21420
|
+
if (typeof ext.cmd === "string") return "stdio";
|
|
21421
|
+
if (typeof ext.uri === "string") return "streamable_http";
|
|
21422
|
+
}
|
|
21423
|
+
/**
|
|
21200
21424
|
* Resolves the canonical timeout for a server (`timeout` or `networkTimeout`).
|
|
21201
21425
|
*/
|
|
21202
21426
|
function resolveGooseTimeout(config) {
|
|
@@ -21222,15 +21446,20 @@ function applyGooseStdioFields(ext, config) {
|
|
|
21222
21446
|
/**
|
|
21223
21447
|
* Converts a single rulesync canonical MCP server into a Goose `extensions:` entry.
|
|
21224
21448
|
*/
|
|
21225
|
-
function convertServerToGooseExtension(name, config) {
|
|
21449
|
+
function convertServerToGooseExtension(name, config, logger) {
|
|
21226
21450
|
const url = resolveGooseUrl(config);
|
|
21227
21451
|
const gooseType = resolveGooseType(config, url);
|
|
21228
21452
|
const ext = {
|
|
21229
21453
|
name,
|
|
21230
21454
|
type: gooseType
|
|
21231
21455
|
};
|
|
21232
|
-
if (gooseType === "stdio")
|
|
21233
|
-
|
|
21456
|
+
if (gooseType === "stdio") {
|
|
21457
|
+
applyGooseStdioFields(ext, config);
|
|
21458
|
+
if (typeof ext.cmd !== "string" || ext.cmd === "") {
|
|
21459
|
+
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.`);
|
|
21460
|
+
return;
|
|
21461
|
+
}
|
|
21462
|
+
} else if (gooseType === "sse" || gooseType === "streamable_http") {
|
|
21234
21463
|
if (url !== void 0) ext.uri = url;
|
|
21235
21464
|
if (isPlainObject$1(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
|
|
21236
21465
|
}
|
|
@@ -21245,13 +21474,41 @@ function convertServerToGooseExtension(name, config) {
|
|
|
21245
21474
|
* Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
|
|
21246
21475
|
* | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
|
|
21247
21476
|
* remote, plus `enabled` and `timeout`.
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21477
|
+
*
|
|
21478
|
+
* `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
|
|
21479
|
+
* holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
|
|
21480
|
+
* (`developer`, `memory`, ...), which have no canonical MCP representation.
|
|
21481
|
+
* Those entries are carried over from `existingExtensions` untouched — removing
|
|
21482
|
+
* `developer` alone costs the agent its shell and text-editor tools. Only an
|
|
21483
|
+
* entry rulesync can positively identify as an MCP server is rulesync's to
|
|
21484
|
+
* replace, so a server deleted from `.rulesync/.mcp.json` is retracted (with a
|
|
21485
|
+
* warning naming it) while an entry of an unrecognized shape or a future
|
|
21486
|
+
* extension type is left alone rather than assumed to be ours.
|
|
21487
|
+
*/
|
|
21488
|
+
function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
|
|
21489
|
+
const generated = {};
|
|
21251
21490
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21252
21491
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
21253
|
-
|
|
21492
|
+
const ext = convertServerToGooseExtension(name, config, logger);
|
|
21493
|
+
if (ext !== void 0) generated[name] = ext;
|
|
21254
21494
|
}
|
|
21495
|
+
const extensions = {};
|
|
21496
|
+
const retracted = [];
|
|
21497
|
+
for (const [name, ext] of Object.entries(existingExtensions)) {
|
|
21498
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
21499
|
+
const type = isRecord(ext) ? existingExtensionType(ext) : void 0;
|
|
21500
|
+
if (type !== void 0 && GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21501
|
+
if (!Object.hasOwn(generated, name)) retracted.push(name);
|
|
21502
|
+
continue;
|
|
21503
|
+
}
|
|
21504
|
+
if (!Object.hasOwn(generated, name)) {
|
|
21505
|
+
extensions[name] = ext;
|
|
21506
|
+
continue;
|
|
21507
|
+
}
|
|
21508
|
+
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.`);
|
|
21509
|
+
}
|
|
21510
|
+
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\`.`);
|
|
21511
|
+
Object.assign(extensions, generated);
|
|
21255
21512
|
return extensions;
|
|
21256
21513
|
}
|
|
21257
21514
|
/**
|
|
@@ -21262,16 +21519,27 @@ function convertToGooseFormat(mcpServers) {
|
|
|
21262
21519
|
* so both `url` and the Claude-specific `httpUrl` alias come back as `url`; and
|
|
21263
21520
|
* the `streamable_http` type maps back to canonical `http`. These are the
|
|
21264
21521
|
* canonical/preferred forms, so re-generating produces an equivalent config.
|
|
21522
|
+
*
|
|
21523
|
+
* Non-MCP extension types (`builtin`, `platform`, `frontend`, `inline_python`)
|
|
21524
|
+
* are skipped: they describe capabilities Goose provides itself, and importing
|
|
21525
|
+
* one would strip the type that makes it work — a `builtin` entry came back as
|
|
21526
|
+
* a `stdio` extension with no `cmd` that Goose cannot start. They stay in `config.yaml`,
|
|
21527
|
+
* which generation preserves.
|
|
21265
21528
|
*/
|
|
21266
21529
|
function convertFromGooseFormat(extensions) {
|
|
21267
21530
|
const result = {};
|
|
21531
|
+
const skipped = [];
|
|
21268
21532
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
21269
21533
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
21534
|
+
const type = existingExtensionType(ext);
|
|
21535
|
+
if (type === void 0 || !GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21536
|
+
skipped.push(name);
|
|
21537
|
+
continue;
|
|
21538
|
+
}
|
|
21270
21539
|
const server = {};
|
|
21271
|
-
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
21272
21540
|
if (type === "sse") server.type = "sse";
|
|
21273
21541
|
else if (type === "streamable_http") server.type = "http";
|
|
21274
|
-
else
|
|
21542
|
+
else server.type = "stdio";
|
|
21275
21543
|
if (typeof ext.cmd === "string") server.command = ext.cmd;
|
|
21276
21544
|
if (isStringArray$1(ext.args)) server.args = ext.args;
|
|
21277
21545
|
if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
|
|
@@ -21281,6 +21549,7 @@ function convertFromGooseFormat(extensions) {
|
|
|
21281
21549
|
if (typeof ext.timeout === "number") server.timeout = ext.timeout;
|
|
21282
21550
|
result[name] = server;
|
|
21283
21551
|
}
|
|
21552
|
+
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
21553
|
return result;
|
|
21285
21554
|
}
|
|
21286
21555
|
/**
|
|
@@ -21402,15 +21671,25 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
21402
21671
|
global
|
|
21403
21672
|
});
|
|
21404
21673
|
}
|
|
21405
|
-
const
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
};
|
|
21674
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21675
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
21676
|
+
const config = parseGooseConfig(existingContent, paths.relativeDirPath, paths.relativeFilePath);
|
|
21677
|
+
const existingExtensions = isRecord(config.extensions) ? config.extensions : {};
|
|
21409
21678
|
return new GooseMcp({
|
|
21410
21679
|
outputRoot,
|
|
21411
21680
|
relativeDirPath: paths.relativeDirPath,
|
|
21412
21681
|
relativeFilePath: paths.relativeFilePath,
|
|
21413
|
-
fileContent:
|
|
21682
|
+
fileContent: applySharedConfigPatch({
|
|
21683
|
+
fileKey: sharedConfigFileKey(paths),
|
|
21684
|
+
feature: "mcp",
|
|
21685
|
+
existingContent,
|
|
21686
|
+
patch: { extensions: convertToGooseFormat({
|
|
21687
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
21688
|
+
existingExtensions,
|
|
21689
|
+
logger
|
|
21690
|
+
}) },
|
|
21691
|
+
filePath
|
|
21692
|
+
}),
|
|
21414
21693
|
validate,
|
|
21415
21694
|
global
|
|
21416
21695
|
});
|
|
@@ -28119,7 +28398,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
|
|
|
28119
28398
|
"interactionMode",
|
|
28120
28399
|
"extraKnownMarketplaces",
|
|
28121
28400
|
"enabledPlugins",
|
|
28122
|
-
"hooksDisabled"
|
|
28401
|
+
"hooksDisabled",
|
|
28402
|
+
"disabledSkills"
|
|
28123
28403
|
];
|
|
28124
28404
|
/**
|
|
28125
28405
|
* Permissions adapter for Factory Droid.
|
|
@@ -32270,6 +32550,30 @@ const CANONICAL_TO_ZED_TOOL_NAMES = {
|
|
|
32270
32550
|
webfetch: "fetch",
|
|
32271
32551
|
websearch: "search_web"
|
|
32272
32552
|
};
|
|
32553
|
+
/**
|
|
32554
|
+
* Canonical categories whose Zed tool is not permission-gated. Zed's gated list
|
|
32555
|
+
* is `terminal`, `edit_file`, `write_file`, `delete_path`, `move_path`,
|
|
32556
|
+
* `copy_path`, `create_directory`, `fetch`, `search_web` and `skill`; the
|
|
32557
|
+
* read-only tools (`read_file`, `grep`, `find_path`, `list_directory`) sit in
|
|
32558
|
+
* Zed's own `EXCLUDED_TOOLS` and never call `decide_permission_from_settings`,
|
|
32559
|
+
* so a `tools.<name>` entry for one is config Zed never consults. Zed's real
|
|
32560
|
+
* read-denial surface is `private_files`, which the ignore feature owns.
|
|
32561
|
+
*
|
|
32562
|
+
* @see https://zed.dev/docs/ai/tool-permissions#supported-tools
|
|
32563
|
+
*/
|
|
32564
|
+
const ZED_EXCLUDED_CANONICAL_CATEGORIES = /* @__PURE__ */ new Set([
|
|
32565
|
+
"read",
|
|
32566
|
+
"grep",
|
|
32567
|
+
"glob"
|
|
32568
|
+
]);
|
|
32569
|
+
/** The Zed-side spellings of the same tools, for a category that names one directly. */
|
|
32570
|
+
const ZED_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
32571
|
+
"read_file",
|
|
32572
|
+
"grep",
|
|
32573
|
+
"find_path",
|
|
32574
|
+
"list_directory"
|
|
32575
|
+
]);
|
|
32576
|
+
const isZedExcludedCategory = (category) => ZED_EXCLUDED_CANONICAL_CATEGORIES.has(category) || ZED_EXCLUDED_TOOL_NAMES.has(toZedToolName(category));
|
|
32273
32577
|
const ZED_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZED_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
32274
32578
|
function toZedToolName(canonical) {
|
|
32275
32579
|
return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
|
|
@@ -32391,15 +32695,21 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
32391
32695
|
const existingTools = asRecord(toolPermissions.tools);
|
|
32392
32696
|
let managedDefault;
|
|
32393
32697
|
const managedTools = {};
|
|
32698
|
+
const excludedCategories = [];
|
|
32394
32699
|
for (const [category, rules] of Object.entries(config.permission)) {
|
|
32395
32700
|
if (category === "*") {
|
|
32396
32701
|
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
32397
32702
|
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.`);
|
|
32398
32703
|
continue;
|
|
32399
32704
|
}
|
|
32705
|
+
if (isZedExcludedCategory(category)) {
|
|
32706
|
+
if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
|
|
32707
|
+
continue;
|
|
32708
|
+
}
|
|
32400
32709
|
const tool = buildZedToolPermission(rules);
|
|
32401
32710
|
if (tool) managedTools[toZedToolName(category)] = tool;
|
|
32402
32711
|
}
|
|
32712
|
+
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\`.`);
|
|
32403
32713
|
const managedToolNames = new Set(Object.keys(managedTools));
|
|
32404
32714
|
if ("*" in config.permission) managedToolNames.add("*");
|
|
32405
32715
|
const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
|
|
@@ -33191,18 +33501,16 @@ var DirFeatureProcessor = class {
|
|
|
33191
33501
|
})) dirHasChanges = true;
|
|
33192
33502
|
}
|
|
33193
33503
|
const otherFiles = aiDir.getOtherFiles();
|
|
33194
|
-
const otherFileContents = [];
|
|
33195
33504
|
for (const file of otherFiles) {
|
|
33196
|
-
|
|
33197
|
-
|
|
33198
|
-
|
|
33199
|
-
|
|
33200
|
-
|
|
33201
|
-
|
|
33202
|
-
|
|
33203
|
-
|
|
33204
|
-
|
|
33205
|
-
}
|
|
33505
|
+
if (dirHasChanges) break;
|
|
33506
|
+
const filePath = join(dirPath, file.relativeFilePathToDirPath);
|
|
33507
|
+
const existingBuffer = await readFileBufferOrNull(filePath);
|
|
33508
|
+
if (!companionFileContentsEquivalent({
|
|
33509
|
+
filePath,
|
|
33510
|
+
expected: file.fileBuffer,
|
|
33511
|
+
existing: existingBuffer,
|
|
33512
|
+
composed: file.composed
|
|
33513
|
+
})) dirHasChanges = true;
|
|
33206
33514
|
}
|
|
33207
33515
|
if (!dirHasChanges) continue;
|
|
33208
33516
|
const relativeDir = aiDir.getRelativePathFromCwd();
|
|
@@ -33222,11 +33530,8 @@ var DirFeatureProcessor = class {
|
|
|
33222
33530
|
await writeFileContent(join(dirPath, mainFile.name), mainFileContent);
|
|
33223
33531
|
changedPaths.push(join(relativeDir, mainFile.name));
|
|
33224
33532
|
}
|
|
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);
|
|
33533
|
+
for (const file of otherFiles) {
|
|
33534
|
+
await writeFileBuffer(join(dirPath, file.relativeFilePathToDirPath), file.fileBuffer);
|
|
33230
33535
|
changedPaths.push(join(relativeDir, file.relativeFilePathToDirPath));
|
|
33231
33536
|
}
|
|
33232
33537
|
}
|
|
@@ -33952,6 +34257,39 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
33952
34257
|
return frontmatter;
|
|
33953
34258
|
}
|
|
33954
34259
|
/**
|
|
34260
|
+
* Escapes the glob metacharacters in a directory path so it matches literally.
|
|
34261
|
+
* A real directory name may contain them — `app/[slug]` in a Next.js tree is
|
|
34262
|
+
* the common case, and unescaped `[slug]` reads as a bracket expression that
|
|
34263
|
+
* matches a different subtree (or nothing at all).
|
|
34264
|
+
*
|
|
34265
|
+
* @see https://code.claude.com/docs/en/memory
|
|
34266
|
+
*/
|
|
34267
|
+
function escapeGlobLiteral(dirPath) {
|
|
34268
|
+
return dirPath.replaceAll(/[\\*?[\]{}()!]/g, "\\$&");
|
|
34269
|
+
}
|
|
34270
|
+
/**
|
|
34271
|
+
* Claude Code scopes a nested skill by its location: a skill living in
|
|
34272
|
+
* `apps/web/.claude/skills/deploy` only activates while working under
|
|
34273
|
+
* `apps/web`. rulesync generates every imported skill into the project-root
|
|
34274
|
+
* `.claude/skills/`, so on import that location-based scoping has to be
|
|
34275
|
+
* re-expressed as an explicit `paths` glob — otherwise the round-trip silently
|
|
34276
|
+
* promotes a subtree skill to global activation.
|
|
34277
|
+
*
|
|
34278
|
+
* Returns the derived glob for a nested discovery root, or `undefined` for the
|
|
34279
|
+
* project-root `.claude/skills` (and for any root whose subtree cannot be
|
|
34280
|
+
* determined), where no scoping is implied.
|
|
34281
|
+
*
|
|
34282
|
+
* @see https://code.claude.com/docs/en/skills
|
|
34283
|
+
*/
|
|
34284
|
+
function deriveNestedSkillPaths(relativeDirPath) {
|
|
34285
|
+
const posixDirPath = toPosixPath(relativeDirPath);
|
|
34286
|
+
const skillsDirSuffix = `/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`;
|
|
34287
|
+
if (!posixDirPath.endsWith(skillsDirSuffix)) return;
|
|
34288
|
+
const subtree = posixDirPath.slice(0, -skillsDirSuffix.length);
|
|
34289
|
+
if (subtree === "" || subtree === ".") return;
|
|
34290
|
+
return [`${escapeGlobLiteral(subtree)}/**`];
|
|
34291
|
+
}
|
|
34292
|
+
/**
|
|
33955
34293
|
* Represents a Claude Code skill directory.
|
|
33956
34294
|
* Unlike subagents and commands, skills are directories containing SKILL.md and other files.
|
|
33957
34295
|
* Extends ToolSkill to inherit directory management and security features from AiDir.
|
|
@@ -34039,6 +34377,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34039
34377
|
}
|
|
34040
34378
|
toRulesyncSkill() {
|
|
34041
34379
|
const frontmatter = this.getFrontmatter();
|
|
34380
|
+
const resolvedPaths = frontmatter.paths !== void 0 ? frontmatter.paths : deriveNestedSkillPaths(this.relativeDirPath);
|
|
34042
34381
|
const claudecodeSection = {
|
|
34043
34382
|
...frontmatter.when_to_use && { when_to_use: frontmatter.when_to_use },
|
|
34044
34383
|
...frontmatter["allowed-tools"] && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
@@ -34055,7 +34394,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34055
34394
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34056
34395
|
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34057
34396
|
...this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true },
|
|
34058
|
-
...
|
|
34397
|
+
...resolvedPaths !== void 0 && { paths: resolvedPaths }
|
|
34059
34398
|
};
|
|
34060
34399
|
const rulesyncFrontmatter = {
|
|
34061
34400
|
name: frontmatter.name,
|
|
@@ -34453,7 +34792,8 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
34453
34792
|
fileBuffer: Buffer.from(dump(openaiObject, {
|
|
34454
34793
|
lineWidth: -1,
|
|
34455
34794
|
noRefs: true
|
|
34456
|
-
}))
|
|
34795
|
+
})),
|
|
34796
|
+
composed: true
|
|
34457
34797
|
}] : baseOtherFiles;
|
|
34458
34798
|
return new CodexCliSkill({
|
|
34459
34799
|
outputRoot,
|
|
@@ -34513,7 +34853,11 @@ const CopilotSkillFrontmatterSchema = z.looseObject({
|
|
|
34513
34853
|
name: z.string(),
|
|
34514
34854
|
description: z.string(),
|
|
34515
34855
|
license: z.optional(z.string()),
|
|
34516
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
34856
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
34857
|
+
"argument-hint": z.optional(z.string()),
|
|
34858
|
+
"user-invocable": z.optional(z.boolean()),
|
|
34859
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
34860
|
+
context: z.optional(z.string())
|
|
34517
34861
|
});
|
|
34518
34862
|
/**
|
|
34519
34863
|
* Represents a GitHub Copilot skill directory.
|
|
@@ -34571,7 +34915,11 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34571
34915
|
const frontmatter = this.getFrontmatter();
|
|
34572
34916
|
const copilotSection = {
|
|
34573
34917
|
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
34574
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
34918
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
34919
|
+
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
|
|
34920
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34921
|
+
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34922
|
+
...frontmatter.context !== void 0 && { context: frontmatter.context }
|
|
34575
34923
|
};
|
|
34576
34924
|
const rulesyncFrontmatter = {
|
|
34577
34925
|
name: frontmatter.name,
|
|
@@ -34593,11 +34941,24 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34593
34941
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34594
34942
|
const settablePaths = CopilotSkill.getSettablePaths({ global });
|
|
34595
34943
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
34944
|
+
const copilotSection = rulesyncFrontmatter.copilot;
|
|
34945
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
34946
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
34947
|
+
section: copilotSection
|
|
34948
|
+
});
|
|
34949
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
34950
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
34951
|
+
section: copilotSection
|
|
34952
|
+
});
|
|
34596
34953
|
const copilotFrontmatter = {
|
|
34597
34954
|
name: rulesyncFrontmatter.name,
|
|
34598
34955
|
description: rulesyncFrontmatter.description,
|
|
34599
|
-
...
|
|
34600
|
-
...
|
|
34956
|
+
...copilotSection?.license !== void 0 && { license: copilotSection.license },
|
|
34957
|
+
...copilotSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotSection["allowed-tools"] },
|
|
34958
|
+
...copilotSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotSection["argument-hint"] },
|
|
34959
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
34960
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
34961
|
+
...copilotSection?.context !== void 0 && { context: copilotSection.context }
|
|
34601
34962
|
};
|
|
34602
34963
|
return new CopilotSkill({
|
|
34603
34964
|
outputRoot,
|
|
@@ -34744,14 +35105,23 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
34744
35105
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34745
35106
|
const settablePaths = CopilotcliSkill.getSettablePaths({ global });
|
|
34746
35107
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35108
|
+
const copilotcliSection = rulesyncFrontmatter.copilotcli;
|
|
35109
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
35110
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35111
|
+
section: copilotcliSection
|
|
35112
|
+
});
|
|
35113
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35114
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35115
|
+
section: copilotcliSection
|
|
35116
|
+
});
|
|
34747
35117
|
const copilotcliFrontmatter = {
|
|
34748
35118
|
name: rulesyncFrontmatter.name,
|
|
34749
35119
|
description: rulesyncFrontmatter.description,
|
|
34750
|
-
...
|
|
34751
|
-
...
|
|
34752
|
-
...
|
|
34753
|
-
...
|
|
34754
|
-
...
|
|
35120
|
+
...copilotcliSection?.license !== void 0 && { license: copilotcliSection.license },
|
|
35121
|
+
...copilotcliSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotcliSection["allowed-tools"] },
|
|
35122
|
+
...copilotcliSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotcliSection["argument-hint"] },
|
|
35123
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35124
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
34755
35125
|
};
|
|
34756
35126
|
return new CopilotcliSkill({
|
|
34757
35127
|
outputRoot,
|
|
@@ -35309,7 +35679,9 @@ const FactorydroidSkillFrontmatterSchema = z.looseObject({
|
|
|
35309
35679
|
name: z.string(),
|
|
35310
35680
|
description: z.string(),
|
|
35311
35681
|
"user-invocable": z.optional(z.boolean()),
|
|
35312
|
-
"disable-model-invocation": z.optional(z.boolean())
|
|
35682
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
35683
|
+
enabled: z.optional(z.boolean()),
|
|
35684
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
35313
35685
|
});
|
|
35314
35686
|
/**
|
|
35315
35687
|
* Represents a Factory Droid skill directory.
|
|
@@ -35365,7 +35737,9 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35365
35737
|
const frontmatter = this.getFrontmatter();
|
|
35366
35738
|
const factorydroidBlock = {
|
|
35367
35739
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
35368
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] }
|
|
35740
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
35741
|
+
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
35742
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
35369
35743
|
};
|
|
35370
35744
|
const rulesyncFrontmatter = {
|
|
35371
35745
|
name: frontmatter.name,
|
|
@@ -35387,19 +35761,22 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35387
35761
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
35388
35762
|
const settablePaths = FactorydroidSkill.getSettablePaths({ global });
|
|
35389
35763
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35764
|
+
const factorydroidSection = rulesyncFrontmatter.factorydroid;
|
|
35390
35765
|
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35391
35766
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35392
|
-
section:
|
|
35767
|
+
section: factorydroidSection
|
|
35393
35768
|
});
|
|
35394
35769
|
const resolvedUserInvocable = resolveUserInvocable({
|
|
35395
35770
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35396
|
-
section:
|
|
35771
|
+
section: factorydroidSection
|
|
35397
35772
|
});
|
|
35398
35773
|
const factorydroidFrontmatter = {
|
|
35399
35774
|
name: rulesyncFrontmatter.name,
|
|
35400
35775
|
description: rulesyncFrontmatter.description,
|
|
35401
35776
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
35402
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
35777
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35778
|
+
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
35779
|
+
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
35403
35780
|
};
|
|
35404
35781
|
return new FactorydroidSkill({
|
|
35405
35782
|
outputRoot,
|
|
@@ -39420,7 +39797,17 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39420
39797
|
validate: true
|
|
39421
39798
|
});
|
|
39422
39799
|
}
|
|
39423
|
-
|
|
39800
|
+
/**
|
|
39801
|
+
* Last chance to adjust the tool frontmatter before it is written. The base
|
|
39802
|
+
* implementation only warns about names Claude Code rejects; plugin-scoped
|
|
39803
|
+
* subclasses extend it to drop fields Claude Code refuses to honor for
|
|
39804
|
+
* plugin-shipped agents.
|
|
39805
|
+
*/
|
|
39806
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
39807
|
+
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.`);
|
|
39808
|
+
return frontmatter;
|
|
39809
|
+
}
|
|
39810
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
39424
39811
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
39425
39812
|
const claudecodeSection = this.filterToolSpecificSection(rulesyncFrontmatter.claudecode ?? {}, ["name", "description"]);
|
|
39426
39813
|
const rawClaudecodeFrontmatter = {
|
|
@@ -39430,7 +39817,11 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39430
39817
|
};
|
|
39431
39818
|
const result = ClaudecodeSubagentFrontmatterSchema.safeParse(rawClaudecodeFrontmatter);
|
|
39432
39819
|
if (!result.success) throw new Error(`Invalid claudecode subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
39433
|
-
const claudecodeFrontmatter =
|
|
39820
|
+
const claudecodeFrontmatter = this.sanitizeFrontmatter({
|
|
39821
|
+
frontmatter: result.data,
|
|
39822
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
39823
|
+
logger
|
|
39824
|
+
});
|
|
39434
39825
|
const body = rulesyncSubagent.getBody();
|
|
39435
39826
|
const fileContent = stringifyFrontmatter(body, claudecodeFrontmatter);
|
|
39436
39827
|
const paths = this.getSettablePaths({ global });
|
|
@@ -39499,6 +39890,21 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39499
39890
|
};
|
|
39500
39891
|
//#endregion
|
|
39501
39892
|
//#region src/features/subagents/claudecode-plugin-subagent.ts
|
|
39893
|
+
/**
|
|
39894
|
+
* Claude Code refuses these for plugin-shipped agents "for security reasons",
|
|
39895
|
+
* so emitting them leaves the author believing the agent is constrained when it
|
|
39896
|
+
* is not. Only these three are dropped: the other fields upstream does not list
|
|
39897
|
+
* (e.g. `color`) are merely ignored, with no misleading security posture.
|
|
39898
|
+
*
|
|
39899
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
39900
|
+
*/
|
|
39901
|
+
const PLUGIN_FORBIDDEN_FIELDS = [
|
|
39902
|
+
"hooks",
|
|
39903
|
+
"mcpServers",
|
|
39904
|
+
"permissionMode"
|
|
39905
|
+
];
|
|
39906
|
+
/** The only `isolation` value plugin agents accept. */
|
|
39907
|
+
const PLUGIN_ISOLATION_VALUE = "worktree";
|
|
39502
39908
|
var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
39503
39909
|
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
39504
39910
|
const targets = rulesyncSubagent.getFrontmatter().targets;
|
|
@@ -39507,6 +39913,21 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
|
39507
39913
|
static getSettablePaths() {
|
|
39508
39914
|
return { relativeDirPath: CLAUDECODE_PLUGIN_AGENTS_DIR };
|
|
39509
39915
|
}
|
|
39916
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
39917
|
+
const sanitized = { ...super.sanitizeFrontmatter({
|
|
39918
|
+
frontmatter,
|
|
39919
|
+
relativeFilePath,
|
|
39920
|
+
logger
|
|
39921
|
+
}) };
|
|
39922
|
+
const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
|
|
39923
|
+
for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
|
|
39924
|
+
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.`);
|
|
39925
|
+
if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
|
|
39926
|
+
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.`);
|
|
39927
|
+
delete sanitized.isolation;
|
|
39928
|
+
}
|
|
39929
|
+
return sanitized;
|
|
39930
|
+
}
|
|
39510
39931
|
};
|
|
39511
39932
|
//#endregion
|
|
39512
39933
|
//#region src/features/subagents/cline-subagent.ts
|
|
@@ -43160,7 +43581,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
43160
43581
|
outputRoot: this.outputRoot,
|
|
43161
43582
|
relativeDirPath: RulesyncSubagent.getSettablePaths().relativeDirPath,
|
|
43162
43583
|
rulesyncSubagent,
|
|
43163
|
-
global: this.global
|
|
43584
|
+
global: this.global,
|
|
43585
|
+
logger: this.logger
|
|
43164
43586
|
}));
|
|
43165
43587
|
}
|
|
43166
43588
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
@@ -45113,7 +45535,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
|
45113
45535
|
};
|
|
45114
45536
|
//#endregion
|
|
45115
45537
|
//#region src/features/rules/copilot-rule.ts
|
|
45116
|
-
const CopilotRuleFrontmatterSchema = z.
|
|
45538
|
+
const CopilotRuleFrontmatterSchema = z.looseObject({
|
|
45117
45539
|
description: z.optional(z.string()),
|
|
45118
45540
|
applyTo: z.optional(z.string()),
|
|
45119
45541
|
name: z.optional(z.string()),
|
|
@@ -45169,15 +45591,13 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45169
45591
|
toRulesyncRule() {
|
|
45170
45592
|
let globs;
|
|
45171
45593
|
if (this.frontmatter.applyTo) globs = this.frontmatter.applyTo.split(",").map((g) => g.trim());
|
|
45594
|
+
const { description, applyTo: _applyTo, ...copilotFields } = this.frontmatter;
|
|
45172
45595
|
const rulesyncFrontmatter = {
|
|
45173
45596
|
targets: ["*"],
|
|
45174
45597
|
root: this.isRoot(),
|
|
45175
|
-
description
|
|
45598
|
+
description,
|
|
45176
45599
|
globs,
|
|
45177
|
-
...(
|
|
45178
|
-
...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
|
|
45179
|
-
...this.frontmatter.name && { name: this.frontmatter.name }
|
|
45180
|
-
} }
|
|
45600
|
+
...Object.keys(copilotFields).length > 0 && { copilot: copilotFields }
|
|
45181
45601
|
};
|
|
45182
45602
|
const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
|
|
45183
45603
|
return new RulesyncRule({
|
|
@@ -45194,10 +45614,9 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45194
45614
|
const root = rulesyncFrontmatter.root;
|
|
45195
45615
|
const paths = this.getSettablePaths({ global });
|
|
45196
45616
|
const copilotFrontmatter = {
|
|
45617
|
+
...rulesyncFrontmatter.copilot,
|
|
45197
45618
|
description: rulesyncFrontmatter.description,
|
|
45198
|
-
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45199
|
-
excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
|
|
45200
|
-
name: rulesyncFrontmatter.copilot?.name
|
|
45619
|
+
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45201
45620
|
};
|
|
45202
45621
|
const body = rulesyncRule.getBody();
|
|
45203
45622
|
if (root) return new CopilotRule({
|
|
@@ -50894,6 +51313,6 @@ async function importChecksCore(params) {
|
|
|
50894
51313
|
return writtenCount;
|
|
50895
51314
|
}
|
|
50896
51315
|
//#endregion
|
|
50897
|
-
export { JsonLogger as $,
|
|
51316
|
+
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
51317
|
|
|
50899
|
-
//# sourceMappingURL=import-
|
|
51318
|
+
//# sourceMappingURL=import-zyfw1Cq_.js.map
|