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
|
@@ -639,6 +639,13 @@ async function readFileBuffer(filepath) {
|
|
|
639
639
|
return (0, node_fs_promises.readFile)(filepath);
|
|
640
640
|
}
|
|
641
641
|
/**
|
|
642
|
+
* Read file as a buffer if it exists, otherwise return null.
|
|
643
|
+
*/
|
|
644
|
+
async function readFileBufferOrNull(filepath) {
|
|
645
|
+
if (await fileExists(filepath)) return readFileBuffer(filepath);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
642
649
|
* Normalizes text to LF line endings and adds exactly one trailing newline.
|
|
643
650
|
* Removes any existing trailing whitespace and appends a single newline.
|
|
644
651
|
*/
|
|
@@ -650,6 +657,10 @@ async function writeFileContent(filepath, content) {
|
|
|
650
657
|
await ensureDir((0, node_path.dirname)(filepath));
|
|
651
658
|
await (0, node_fs_promises.writeFile)(filepath, content, "utf-8");
|
|
652
659
|
}
|
|
660
|
+
async function writeFileBuffer(filepath, buffer) {
|
|
661
|
+
await ensureDir((0, node_path.dirname)(filepath));
|
|
662
|
+
await (0, node_fs_promises.writeFile)(filepath, buffer);
|
|
663
|
+
}
|
|
653
664
|
async function fileExists(filepath) {
|
|
654
665
|
try {
|
|
655
666
|
await (0, node_fs_promises.stat)(filepath);
|
|
@@ -2182,6 +2193,7 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
|
|
|
2182
2193
|
name: zod_mini.z.optional(safeString),
|
|
2183
2194
|
description: zod_mini.z.optional(safeString),
|
|
2184
2195
|
failClosed: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2196
|
+
commandRegex: zod_mini.z.optional(safeString),
|
|
2185
2197
|
sequential: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2186
2198
|
async: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2187
2199
|
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), safeString)),
|
|
@@ -2198,6 +2210,7 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
|
|
|
2198
2210
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
2199
2211
|
if: zod_mini.z.optional(safeString),
|
|
2200
2212
|
commandWindows: zod_mini.z.optional(safeString),
|
|
2213
|
+
additionalContextLimit: zod_mini.z.optional(zod_mini.z.int().check((0, zod_mini.nonnegative)())),
|
|
2201
2214
|
asyncRewake: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
2202
2215
|
continueOnBlock: zod_mini.z.optional(zod_mini.z.boolean())
|
|
2203
2216
|
});
|
|
@@ -4245,7 +4258,8 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4245
4258
|
*/
|
|
4246
4259
|
const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
4247
4260
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
4248
|
-
commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
4261
|
+
commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
4262
|
+
disabledSkills: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
4249
4263
|
});
|
|
4250
4264
|
/**
|
|
4251
4265
|
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
@@ -4691,13 +4705,22 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
|
|
|
4691
4705
|
* `base_permission_profile` it is consumed by the profile builder, not
|
|
4692
4706
|
* written as a top-level config key.
|
|
4693
4707
|
*
|
|
4694
|
-
*
|
|
4695
|
-
*
|
|
4696
|
-
*
|
|
4697
|
-
*
|
|
4698
|
-
*
|
|
4699
|
-
*
|
|
4700
|
-
*
|
|
4708
|
+
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
4709
|
+
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
4710
|
+
* (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
|
|
4711
|
+
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
|
|
4712
|
+
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
4713
|
+
* `base_permission_profile` and `git_write_rules` are consumed by the profile
|
|
4714
|
+
* builder rather than written, as described above, and `permission` is the
|
|
4715
|
+
* tool-scoped canonical block, which `RulesyncPermissions.forTarget` strips out
|
|
4716
|
+
* of the override before it ever reaches the patch. The allowlist is what keeps
|
|
4717
|
+
* the override from clobbering a feature-owned key: `mcp_servers.*` per-MCP
|
|
4718
|
+
* gating is owned by the MCP feature (`codexcli-mcp.ts` already writes the
|
|
4719
|
+
* `mcp_servers` tables in the same `config.toml`), and `permissions` /
|
|
4720
|
+
* `default_permissions` are owned by the canonical model. The schema itself is
|
|
4721
|
+
* `looseObject` so an unmodeled key parses (and is then reported rather than
|
|
4722
|
+
* rejected outright); supporting a new top-level Codex config key means adding
|
|
4723
|
+
* it to `CODEXCLI_OVERRIDE_KEYS`.
|
|
4701
4724
|
*
|
|
4702
4725
|
* @see https://developers.openai.com/codex/config-reference
|
|
4703
4726
|
* @see https://developers.openai.com/codex/permissions
|
|
@@ -5172,7 +5195,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5172
5195
|
})),
|
|
5173
5196
|
copilot: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5174
5197
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
5175
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
5198
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
5199
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string()),
|
|
5200
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5201
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5202
|
+
context: zod_mini.z.optional(zod_mini.z.string())
|
|
5176
5203
|
})),
|
|
5177
5204
|
copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5178
5205
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -5230,7 +5257,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5230
5257
|
})),
|
|
5231
5258
|
factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5232
5259
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5233
|
-
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean())
|
|
5260
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5261
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5262
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
5234
5263
|
})),
|
|
5235
5264
|
grokcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5236
5265
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
@@ -5443,8 +5472,8 @@ async function getLocalSkillDirNames(outputRoot) {
|
|
|
5443
5472
|
* Resolve the effective `disable-model-invocation` value for a tool skill.
|
|
5444
5473
|
*
|
|
5445
5474
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
5446
|
-
* default that applies to every tool supporting the flag (claudecode,
|
|
5447
|
-
* zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5475
|
+
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
5476
|
+
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
5448
5477
|
* default with a per-target value. A defined section value (including `false`)
|
|
5449
5478
|
* always wins over the root default.
|
|
5450
5479
|
*
|
|
@@ -5457,8 +5486,8 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
5457
5486
|
* Resolve the effective `user-invocable` value for a tool skill.
|
|
5458
5487
|
*
|
|
5459
5488
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
5460
|
-
* that applies to every tool supporting the flag (claudecode,
|
|
5461
|
-
* grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5489
|
+
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
5490
|
+
* copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
5462
5491
|
* per-target value. A defined section value (including `false`) always wins
|
|
5463
5492
|
* over the root default.
|
|
5464
5493
|
*
|
|
@@ -5671,6 +5700,31 @@ function fileContentsEquivalent({ filePath, expected, existing }) {
|
|
|
5671
5700
|
if (structured !== void 0) return structured;
|
|
5672
5701
|
return addTrailingNewline(expected) === addTrailingNewline(existing);
|
|
5673
5702
|
}
|
|
5703
|
+
/**
|
|
5704
|
+
* Whether an on-disk companion file is equivalent to the generated one.
|
|
5705
|
+
*
|
|
5706
|
+
* Companion files (everything beside a skill's `SKILL.md`) are written byte for
|
|
5707
|
+
* byte, so byte equality is the whole test for a user asset carried through
|
|
5708
|
+
* from the source directory: a CRLF fixture or a deliberately newline-less file
|
|
5709
|
+
* must compare equal to itself and unequal to a normalized copy, and a copy
|
|
5710
|
+
* that has drifted must be repaired rather than tolerated.
|
|
5711
|
+
*
|
|
5712
|
+
* A `composed` file is different — Rulesync builds it from frontmatter (Codex
|
|
5713
|
+
* CLI's `agents/openai.yaml`), so differing bytes fall back to the structured
|
|
5714
|
+
* comparison and a formatter re-indenting it is not reported as a change on
|
|
5715
|
+
* every generate. Only the structured verdict counts: there is deliberately no
|
|
5716
|
+
* text fallback, since trailing-whitespace-insensitive text equality is exactly
|
|
5717
|
+
* the normalization companion files no longer get.
|
|
5718
|
+
*/
|
|
5719
|
+
function companionFileContentsEquivalent({ filePath, expected, existing, composed = false }) {
|
|
5720
|
+
if (existing === null) return false;
|
|
5721
|
+
if (existing.equals(expected)) return true;
|
|
5722
|
+
if (!composed) return false;
|
|
5723
|
+
const expectedText = expected.toString("utf-8");
|
|
5724
|
+
const existingText = existing.toString("utf-8");
|
|
5725
|
+
if (!Buffer.from(expectedText, "utf-8").equals(expected) || !Buffer.from(existingText, "utf-8").equals(existing)) return false;
|
|
5726
|
+
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
5727
|
+
}
|
|
5674
5728
|
//#endregion
|
|
5675
5729
|
//#region src/types/feature-processor.ts
|
|
5676
5730
|
var FeatureProcessor = class {
|
|
@@ -7071,6 +7125,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7071
7125
|
ownedKeys: ["mcp", "tools"]
|
|
7072
7126
|
} }
|
|
7073
7127
|
},
|
|
7128
|
+
".config/goose/config.yaml": {
|
|
7129
|
+
format: "yaml",
|
|
7130
|
+
invalidRootPolicy: "error",
|
|
7131
|
+
features: { mcp: {
|
|
7132
|
+
kind: "replace-owned-keys",
|
|
7133
|
+
ownedKeys: ["extensions"]
|
|
7134
|
+
} }
|
|
7135
|
+
},
|
|
7074
7136
|
[CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
|
|
7075
7137
|
format: "toml",
|
|
7076
7138
|
features: {
|
|
@@ -9309,9 +9371,10 @@ const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
|
9309
9371
|
//#region src/features/commands/factorydroid-command.ts
|
|
9310
9372
|
const FactorydroidCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
9311
9373
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
9312
|
-
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
9313
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
9374
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
9314
9375
|
});
|
|
9376
|
+
/** Not a Droid command surface; see the schema comment above. */
|
|
9377
|
+
const FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS = ["allowed-tools"];
|
|
9315
9378
|
var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
9316
9379
|
frontmatter;
|
|
9317
9380
|
body;
|
|
@@ -9338,6 +9401,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9338
9401
|
}
|
|
9339
9402
|
toRulesyncCommand() {
|
|
9340
9403
|
const { description, ...restFields } = this.frontmatter;
|
|
9404
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete restFields[field];
|
|
9341
9405
|
const rulesyncFrontmatter = {
|
|
9342
9406
|
targets: ["*"],
|
|
9343
9407
|
description,
|
|
@@ -9361,6 +9425,7 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9361
9425
|
description: rulesyncFrontmatter.description,
|
|
9362
9426
|
...factorydroidFields
|
|
9363
9427
|
};
|
|
9428
|
+
for (const field of FACTORYDROID_UNSUPPORTED_COMMAND_FIELDS) delete factorydroidFrontmatter[field];
|
|
9364
9429
|
const body = rulesyncCommand.getBody();
|
|
9365
9430
|
const paths = this.getSettablePaths({ global });
|
|
9366
9431
|
return new FactorydroidCommand({
|
|
@@ -13072,16 +13137,28 @@ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
|
|
|
13072
13137
|
};
|
|
13073
13138
|
}
|
|
13074
13139
|
/**
|
|
13075
|
-
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13076
|
-
* preserving insertion order of both keys and grouped definitions.
|
|
13140
|
+
* Group a list of hook definitions by their `matcher` (empty string when
|
|
13141
|
+
* absent), preserving insertion order of both keys and grouped definitions.
|
|
13142
|
+
* Definitions that disagree on a `subdividesGroup` passthrough field are split
|
|
13143
|
+
* into separate groups, so a restricting field is never inherited by a hook
|
|
13144
|
+
* that did not ask for it.
|
|
13077
13145
|
*/
|
|
13078
|
-
function groupDefinitionsByMatcher(definitions) {
|
|
13146
|
+
function groupDefinitionsByMatcher({ definitions, converterConfig }) {
|
|
13147
|
+
const subdividingFields = (converterConfig.groupPassthroughFields ?? []).filter(({ subdividesGroup }) => subdividesGroup);
|
|
13079
13148
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
13080
13149
|
for (const def of definitions) {
|
|
13081
|
-
const
|
|
13082
|
-
const
|
|
13083
|
-
|
|
13084
|
-
|
|
13150
|
+
const rawMatcher = def.matcher ?? "";
|
|
13151
|
+
const matcher = converterConfig.wildcardMatcherMeansAll && rawMatcher === "*" ? "" : rawMatcher;
|
|
13152
|
+
const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
|
|
13153
|
+
const value = def[canonical];
|
|
13154
|
+
return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
13155
|
+
})].join("\0");
|
|
13156
|
+
const group = byMatcher.get(key);
|
|
13157
|
+
if (group) group.defs.push(def);
|
|
13158
|
+
else byMatcher.set(key, {
|
|
13159
|
+
matcher,
|
|
13160
|
+
defs: [def]
|
|
13161
|
+
});
|
|
13085
13162
|
}
|
|
13086
13163
|
return byMatcher;
|
|
13087
13164
|
}
|
|
@@ -13126,6 +13203,24 @@ function importBooleanPassthroughFields({ h, converterConfig }) {
|
|
|
13126
13203
|
return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ tool }) => typeof h[tool] === "boolean").map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13127
13204
|
}
|
|
13128
13205
|
/**
|
|
13206
|
+
* Emit the configured number passthrough fields on the tool side, mapping each
|
|
13207
|
+
* canonical field name to its (possibly renamed) tool field name. Only finite
|
|
13208
|
+
* numbers are carried through.
|
|
13209
|
+
*/
|
|
13210
|
+
function emitNumberPassthroughFields({ def, hookType, converterConfig }) {
|
|
13211
|
+
return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13212
|
+
if (commandOnly === true && hookType !== "command") return false;
|
|
13213
|
+
return Number.isFinite(def[canonical]);
|
|
13214
|
+
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13215
|
+
}
|
|
13216
|
+
/**
|
|
13217
|
+
* Import the configured number passthrough fields back into canonical fields,
|
|
13218
|
+
* reversing {@link emitNumberPassthroughFields}. Only finite numbers are read.
|
|
13219
|
+
*/
|
|
13220
|
+
function importNumberPassthroughFields({ h, converterConfig }) {
|
|
13221
|
+
return Object.fromEntries((converterConfig.numberPassthroughFields ?? []).filter(({ tool }) => Number.isFinite(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13222
|
+
}
|
|
13223
|
+
/**
|
|
13129
13224
|
* Emit the configured string passthrough fields on the tool side, mapping each
|
|
13130
13225
|
* canonical field name to its (possibly renamed) tool field name. Only non-empty
|
|
13131
13226
|
* string values are carried through.
|
|
@@ -13163,17 +13258,45 @@ function importArrayPassthroughFields({ h, converterConfig, logger }) {
|
|
|
13163
13258
|
return Object.fromEntries(fields.filter(({ tool }) => isSafeStringArray(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13164
13259
|
}
|
|
13165
13260
|
/**
|
|
13261
|
+
* Emit the configured string-map passthrough fields on the tool side. Only maps
|
|
13262
|
+
* whose values are all control-character-free strings are carried through.
|
|
13263
|
+
*/
|
|
13264
|
+
function emitRecordPassthroughFields({ def, hookType, converterConfig }) {
|
|
13265
|
+
return Object.fromEntries((converterConfig.recordPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
|
|
13266
|
+
if (commandOnly === true && hookType !== "command") return false;
|
|
13267
|
+
return isSafeStringRecord(def[canonical]);
|
|
13268
|
+
}).map(({ canonical, tool }) => [tool, def[canonical]]));
|
|
13269
|
+
}
|
|
13270
|
+
/**
|
|
13271
|
+
* Import the configured string-map passthrough fields, reversing
|
|
13272
|
+
* {@link emitRecordPassthroughFields}.
|
|
13273
|
+
*/
|
|
13274
|
+
function importRecordPassthroughFields({ h, hookType, converterConfig, logger }) {
|
|
13275
|
+
const fields = (converterConfig.recordPassthroughFields ?? []).filter(({ commandOnly }) => commandOnly !== true || hookType === "command");
|
|
13276
|
+
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.`);
|
|
13277
|
+
return Object.fromEntries(fields.filter(({ tool }) => isSafeStringRecord(h[tool])).map(({ canonical, tool }) => [canonical, h[tool]]));
|
|
13278
|
+
}
|
|
13279
|
+
/**
|
|
13280
|
+
* Check a value against the shape its field documents. A string field also
|
|
13281
|
+
* rejects control characters, matching the canonical `safeString` so an
|
|
13282
|
+
* imported value cannot fail validation on the next generate.
|
|
13283
|
+
*/
|
|
13284
|
+
function isGroupPassthroughValue(value, valueType = "object") {
|
|
13285
|
+
if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
|
|
13286
|
+
return isPlainObject$1(value);
|
|
13287
|
+
}
|
|
13288
|
+
/**
|
|
13166
13289
|
* Emit the configured group-level passthrough fields, taken from the first
|
|
13167
13290
|
* definition of the group that carries one.
|
|
13168
13291
|
*/
|
|
13169
13292
|
function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
|
|
13170
13293
|
const emitted = {};
|
|
13171
|
-
for (const { canonical, tool } of converterConfig.groupPassthroughFields ?? []) {
|
|
13294
|
+
for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
|
|
13172
13295
|
const carried = defs.map((def) => def[canonical]);
|
|
13173
|
-
const first = carried.find((value) =>
|
|
13296
|
+
const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
|
|
13174
13297
|
if (first === void 0) continue;
|
|
13175
13298
|
const firstStable = stableJson(first);
|
|
13176
|
-
const agrees = (value) =>
|
|
13299
|
+
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
13177
13300
|
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${JSON.stringify(first)} — including any that asked for something else, or for nothing.`);
|
|
13178
13301
|
emitted[tool] = first;
|
|
13179
13302
|
}
|
|
@@ -13185,7 +13308,7 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
13185
13308
|
*/
|
|
13186
13309
|
function importGroupPassthroughFields({ rawEntry, converterConfig }) {
|
|
13187
13310
|
const entry = rawEntry;
|
|
13188
|
-
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool }) =>
|
|
13311
|
+
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
13189
13312
|
}
|
|
13190
13313
|
/**
|
|
13191
13314
|
* Emit the payload fields specific to a hook type — `url`/`headers`/
|
|
@@ -13231,6 +13354,11 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13231
13354
|
hookType,
|
|
13232
13355
|
converterConfig
|
|
13233
13356
|
}),
|
|
13357
|
+
...emitNumberPassthroughFields({
|
|
13358
|
+
def,
|
|
13359
|
+
hookType,
|
|
13360
|
+
converterConfig
|
|
13361
|
+
}),
|
|
13234
13362
|
...emitStringPassthroughFields({
|
|
13235
13363
|
def,
|
|
13236
13364
|
hookType,
|
|
@@ -13241,6 +13369,11 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
13241
13369
|
hookType,
|
|
13242
13370
|
converterConfig
|
|
13243
13371
|
}),
|
|
13372
|
+
...emitRecordPassthroughFields({
|
|
13373
|
+
def,
|
|
13374
|
+
hookType,
|
|
13375
|
+
converterConfig
|
|
13376
|
+
}),
|
|
13244
13377
|
type: hookType,
|
|
13245
13378
|
...command !== void 0 && command !== null && { command },
|
|
13246
13379
|
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
@@ -13271,10 +13404,13 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
13271
13404
|
const result = {};
|
|
13272
13405
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
13273
13406
|
const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
|
|
13274
|
-
const byMatcher = groupDefinitionsByMatcher(
|
|
13407
|
+
const byMatcher = groupDefinitionsByMatcher({
|
|
13408
|
+
definitions,
|
|
13409
|
+
converterConfig
|
|
13410
|
+
});
|
|
13275
13411
|
const entries = [];
|
|
13276
13412
|
const isNoMatcherEvent = converterConfig.noMatcherEvents?.has(eventName) ?? false;
|
|
13277
|
-
for (const
|
|
13413
|
+
for (const { matcher: matcherKey, defs } of byMatcher.values()) {
|
|
13278
13414
|
if (isNoMatcherEvent && matcherKey) logger?.warn(`matcher "${matcherKey}" on "${eventName}" hook will be ignored — this event does not support matchers`);
|
|
13279
13415
|
const hooks = buildToolHooks({
|
|
13280
13416
|
defs,
|
|
@@ -13354,9 +13490,27 @@ function isStringArray(value) {
|
|
|
13354
13490
|
}
|
|
13355
13491
|
/** Compare object values without letting key order decide the answer. */
|
|
13356
13492
|
function stableJson(value) {
|
|
13493
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
13357
13494
|
return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
|
|
13358
13495
|
}
|
|
13359
13496
|
/**
|
|
13497
|
+
* A string map safe to hand a tool as a hook's environment block. On top of
|
|
13498
|
+
* {@link isStringRecord} it rejects a non-plain object (a class instance is not
|
|
13499
|
+
* data) and applies the control-character rule to the values, as
|
|
13500
|
+
* {@link isSafeStringArray} does for `args`.
|
|
13501
|
+
*
|
|
13502
|
+
* The keys are checked more strictly than the values. A tool builds each entry
|
|
13503
|
+
* back into a `KEY=VALUE` string for the spawned process, so a key holding `=`
|
|
13504
|
+
* (or a control character, or nothing at all) names a different variable than
|
|
13505
|
+
* it appears to — `PATH=/tmp/evil` written as a key would set `PATH`. An
|
|
13506
|
+
* authored `.rulesync/hooks.*` can arrive via `rulesync fetch`, so that is not
|
|
13507
|
+
* a shape to pass along.
|
|
13508
|
+
*/
|
|
13509
|
+
function isSafeStringRecord(value) {
|
|
13510
|
+
if (!isPlainObject$1(value) || !isStringRecord(value)) return false;
|
|
13511
|
+
return Object.entries(value).every(([key, entry]) => key !== "" && !key.includes("=") && !CONTROL_CHARS.some((char) => key.includes(char) || entry.includes(char)));
|
|
13512
|
+
}
|
|
13513
|
+
/**
|
|
13360
13514
|
* Control characters cannot ride from an existing tool config into a canonical
|
|
13361
13515
|
* field the schema guards with `safeString`, or the next generate fails
|
|
13362
13516
|
* validation on a file this import itself wrote — and the hooks feature is
|
|
@@ -13409,6 +13563,10 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
|
|
|
13409
13563
|
h,
|
|
13410
13564
|
converterConfig
|
|
13411
13565
|
}),
|
|
13566
|
+
...importNumberPassthroughFields({
|
|
13567
|
+
h,
|
|
13568
|
+
converterConfig
|
|
13569
|
+
}),
|
|
13412
13570
|
...importStringPassthroughFields({
|
|
13413
13571
|
h,
|
|
13414
13572
|
converterConfig
|
|
@@ -13418,6 +13576,12 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, logger }) {
|
|
|
13418
13576
|
converterConfig,
|
|
13419
13577
|
logger
|
|
13420
13578
|
}),
|
|
13579
|
+
...importRecordPassthroughFields({
|
|
13580
|
+
h,
|
|
13581
|
+
hookType,
|
|
13582
|
+
converterConfig,
|
|
13583
|
+
logger
|
|
13584
|
+
}),
|
|
13421
13585
|
...importGroupPassthroughFields({
|
|
13422
13586
|
rawEntry,
|
|
13423
13587
|
converterConfig
|
|
@@ -13935,6 +14099,14 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13935
14099
|
isDeletable() {
|
|
13936
14100
|
return false;
|
|
13937
14101
|
}
|
|
14102
|
+
/**
|
|
14103
|
+
* The converter config used for both directions. Exposed as a static hook so
|
|
14104
|
+
* plugin-scoped subclasses can swap tool-specific details (e.g. the project
|
|
14105
|
+
* directory variable) without duplicating the rest of the config.
|
|
14106
|
+
*/
|
|
14107
|
+
static getConverterConfig() {
|
|
14108
|
+
return CLAUDE_CONVERTER_CONFIG;
|
|
14109
|
+
}
|
|
13938
14110
|
static getSettablePaths(_options = {}) {
|
|
13939
14111
|
return {
|
|
13940
14112
|
relativeDirPath: CLAUDECODE_DIR,
|
|
@@ -13960,7 +14132,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13960
14132
|
const claudeHooks = canonicalToToolHooks({
|
|
13961
14133
|
config,
|
|
13962
14134
|
toolOverrideHooks: config.claudecode?.hooks,
|
|
13963
|
-
converterConfig:
|
|
14135
|
+
converterConfig: this.getConverterConfig(),
|
|
13964
14136
|
logger
|
|
13965
14137
|
});
|
|
13966
14138
|
const fileContent = applySharedConfigPatch({
|
|
@@ -13987,7 +14159,7 @@ var ClaudecodeHooks = class extends ToolHooks {
|
|
|
13987
14159
|
}
|
|
13988
14160
|
const hooks = toolHooksToCanonical({
|
|
13989
14161
|
hooks: settings.hooks,
|
|
13990
|
-
converterConfig:
|
|
14162
|
+
converterConfig: this.constructor.getConverterConfig()
|
|
13991
14163
|
});
|
|
13992
14164
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
13993
14165
|
hooks,
|
|
@@ -14016,6 +14188,21 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
|
14016
14188
|
isDeletable() {
|
|
14017
14189
|
return true;
|
|
14018
14190
|
}
|
|
14191
|
+
/**
|
|
14192
|
+
* Plugin hook scripts ship inside the plugin, so their commands must resolve
|
|
14193
|
+
* against the plugin install directory rather than the consumer's project
|
|
14194
|
+
* root. Upstream documents `"${CLAUDE_PLUGIN_ROOT}"/scripts/format-code.sh`;
|
|
14195
|
+
* `$CLAUDE_PROJECT_DIR` would expand to a path in the consumer's own repo,
|
|
14196
|
+
* where the bundled script does not exist.
|
|
14197
|
+
*
|
|
14198
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
14199
|
+
*/
|
|
14200
|
+
static getConverterConfig() {
|
|
14201
|
+
return {
|
|
14202
|
+
...super.getConverterConfig(),
|
|
14203
|
+
projectDirVar: "$CLAUDE_PLUGIN_ROOT"
|
|
14204
|
+
};
|
|
14205
|
+
}
|
|
14019
14206
|
static getSettablePaths() {
|
|
14020
14207
|
return {
|
|
14021
14208
|
relativeDirPath: CLAUDECODE_PLUGIN_HOOKS_DIR,
|
|
@@ -14038,6 +14225,10 @@ const CODEXCLI_CONVERTER_CONFIG = {
|
|
|
14038
14225
|
}, {
|
|
14039
14226
|
canonical: "statusMessage",
|
|
14040
14227
|
tool: "statusMessage"
|
|
14228
|
+
}],
|
|
14229
|
+
numberPassthroughFields: [{
|
|
14230
|
+
canonical: "additionalContextLimit",
|
|
14231
|
+
tool: "additionalContextLimit"
|
|
14041
14232
|
}]
|
|
14042
14233
|
};
|
|
14043
14234
|
/**
|
|
@@ -15081,7 +15272,13 @@ const FACTORYDROID_CONVERTER_CONFIG = {
|
|
|
15081
15272
|
toolToCanonicalEventNames: FACTORYDROID_TO_CANONICAL_EVENT_NAMES,
|
|
15082
15273
|
projectDirVar: "$FACTORY_PROJECT_DIR",
|
|
15083
15274
|
prefixDotRelativeCommandsOnly: true,
|
|
15084
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15275
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15276
|
+
groupPassthroughFields: [{
|
|
15277
|
+
canonical: "commandRegex",
|
|
15278
|
+
tool: "commandRegex",
|
|
15279
|
+
valueType: "string",
|
|
15280
|
+
subdividesGroup: true
|
|
15281
|
+
}]
|
|
15085
15282
|
};
|
|
15086
15283
|
var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
15087
15284
|
constructor(params) {
|
|
@@ -15177,7 +15374,8 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
15177
15374
|
canonicalToToolEventNames: CANONICAL_TO_GOOSE_EVENT_NAMES,
|
|
15178
15375
|
toolToCanonicalEventNames: GOOSE_TO_CANONICAL_EVENT_NAMES,
|
|
15179
15376
|
projectDirVar: "",
|
|
15180
|
-
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
15377
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
15378
|
+
wildcardMatcherMeansAll: true
|
|
15181
15379
|
};
|
|
15182
15380
|
/**
|
|
15183
15381
|
* Represents a Goose lifecycle hooks file.
|
|
@@ -15272,6 +15470,11 @@ const GROKCLI_CONVERTER_CONFIG = {
|
|
|
15272
15470
|
toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
|
|
15273
15471
|
projectDirVar: "",
|
|
15274
15472
|
supportedHookTypes: /* @__PURE__ */ new Set(["command", "http"]),
|
|
15473
|
+
recordPassthroughFields: [{
|
|
15474
|
+
canonical: "env",
|
|
15475
|
+
tool: "env",
|
|
15476
|
+
commandOnly: true
|
|
15477
|
+
}],
|
|
15275
15478
|
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
15276
15479
|
"sessionStart",
|
|
15277
15480
|
"sessionEnd",
|
|
@@ -21222,6 +21425,27 @@ function resolveGooseType(config, url) {
|
|
|
21222
21425
|
return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
|
|
21223
21426
|
}
|
|
21224
21427
|
/**
|
|
21428
|
+
* The Goose extension types that carry an MCP server. Goose also documents
|
|
21429
|
+
* `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
|
|
21430
|
+
* no canonical MCP counterpart: they name capabilities Goose provides itself
|
|
21431
|
+
* rather than a server rulesync could describe.
|
|
21432
|
+
*/
|
|
21433
|
+
const GOOSE_MCP_EXTENSION_TYPES = /* @__PURE__ */ new Set([
|
|
21434
|
+
"stdio",
|
|
21435
|
+
"streamable_http",
|
|
21436
|
+
"sse"
|
|
21437
|
+
]);
|
|
21438
|
+
/**
|
|
21439
|
+
* Resolves the Goose extension type of an existing `extensions:` entry the way
|
|
21440
|
+
* Goose itself reads it: the declared `type`, or the shape of the entry when
|
|
21441
|
+
* the key is absent.
|
|
21442
|
+
*/
|
|
21443
|
+
function existingExtensionType(ext) {
|
|
21444
|
+
if (typeof ext.type === "string") return ext.type;
|
|
21445
|
+
if (typeof ext.cmd === "string") return "stdio";
|
|
21446
|
+
if (typeof ext.uri === "string") return "streamable_http";
|
|
21447
|
+
}
|
|
21448
|
+
/**
|
|
21225
21449
|
* Resolves the canonical timeout for a server (`timeout` or `networkTimeout`).
|
|
21226
21450
|
*/
|
|
21227
21451
|
function resolveGooseTimeout(config) {
|
|
@@ -21247,15 +21471,20 @@ function applyGooseStdioFields(ext, config) {
|
|
|
21247
21471
|
/**
|
|
21248
21472
|
* Converts a single rulesync canonical MCP server into a Goose `extensions:` entry.
|
|
21249
21473
|
*/
|
|
21250
|
-
function convertServerToGooseExtension(name, config) {
|
|
21474
|
+
function convertServerToGooseExtension(name, config, logger) {
|
|
21251
21475
|
const url = resolveGooseUrl(config);
|
|
21252
21476
|
const gooseType = resolveGooseType(config, url);
|
|
21253
21477
|
const ext = {
|
|
21254
21478
|
name,
|
|
21255
21479
|
type: gooseType
|
|
21256
21480
|
};
|
|
21257
|
-
if (gooseType === "stdio")
|
|
21258
|
-
|
|
21481
|
+
if (gooseType === "stdio") {
|
|
21482
|
+
applyGooseStdioFields(ext, config);
|
|
21483
|
+
if (typeof ext.cmd !== "string" || ext.cmd === "") {
|
|
21484
|
+
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.`);
|
|
21485
|
+
return;
|
|
21486
|
+
}
|
|
21487
|
+
} else if (gooseType === "sse" || gooseType === "streamable_http") {
|
|
21259
21488
|
if (url !== void 0) ext.uri = url;
|
|
21260
21489
|
if (isPlainObject$1(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
|
|
21261
21490
|
}
|
|
@@ -21270,13 +21499,41 @@ function convertServerToGooseExtension(name, config) {
|
|
|
21270
21499
|
* Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
|
|
21271
21500
|
* | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
|
|
21272
21501
|
* remote, plus `enabled` and `timeout`.
|
|
21273
|
-
|
|
21274
|
-
|
|
21275
|
-
|
|
21502
|
+
*
|
|
21503
|
+
* `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
|
|
21504
|
+
* holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
|
|
21505
|
+
* (`developer`, `memory`, ...), which have no canonical MCP representation.
|
|
21506
|
+
* Those entries are carried over from `existingExtensions` untouched — removing
|
|
21507
|
+
* `developer` alone costs the agent its shell and text-editor tools. Only an
|
|
21508
|
+
* entry rulesync can positively identify as an MCP server is rulesync's to
|
|
21509
|
+
* replace, so a server deleted from `.rulesync/.mcp.json` is retracted (with a
|
|
21510
|
+
* warning naming it) while an entry of an unrecognized shape or a future
|
|
21511
|
+
* extension type is left alone rather than assumed to be ours.
|
|
21512
|
+
*/
|
|
21513
|
+
function convertToGooseFormat({ mcpServers, existingExtensions, logger }) {
|
|
21514
|
+
const generated = {};
|
|
21276
21515
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
21277
21516
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
21278
|
-
|
|
21517
|
+
const ext = convertServerToGooseExtension(name, config, logger);
|
|
21518
|
+
if (ext !== void 0) generated[name] = ext;
|
|
21279
21519
|
}
|
|
21520
|
+
const extensions = {};
|
|
21521
|
+
const retracted = [];
|
|
21522
|
+
for (const [name, ext] of Object.entries(existingExtensions)) {
|
|
21523
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
21524
|
+
const type = isRecord(ext) ? existingExtensionType(ext) : void 0;
|
|
21525
|
+
if (type !== void 0 && GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21526
|
+
if (!Object.hasOwn(generated, name)) retracted.push(name);
|
|
21527
|
+
continue;
|
|
21528
|
+
}
|
|
21529
|
+
if (!Object.hasOwn(generated, name)) {
|
|
21530
|
+
extensions[name] = ext;
|
|
21531
|
+
continue;
|
|
21532
|
+
}
|
|
21533
|
+
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.`);
|
|
21534
|
+
}
|
|
21535
|
+
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\`.`);
|
|
21536
|
+
Object.assign(extensions, generated);
|
|
21280
21537
|
return extensions;
|
|
21281
21538
|
}
|
|
21282
21539
|
/**
|
|
@@ -21287,16 +21544,27 @@ function convertToGooseFormat(mcpServers) {
|
|
|
21287
21544
|
* so both `url` and the Claude-specific `httpUrl` alias come back as `url`; and
|
|
21288
21545
|
* the `streamable_http` type maps back to canonical `http`. These are the
|
|
21289
21546
|
* canonical/preferred forms, so re-generating produces an equivalent config.
|
|
21547
|
+
*
|
|
21548
|
+
* Non-MCP extension types (`builtin`, `platform`, `frontend`, `inline_python`)
|
|
21549
|
+
* are skipped: they describe capabilities Goose provides itself, and importing
|
|
21550
|
+
* one would strip the type that makes it work — a `builtin` entry came back as
|
|
21551
|
+
* a `stdio` extension with no `cmd` that Goose cannot start. They stay in `config.yaml`,
|
|
21552
|
+
* which generation preserves.
|
|
21290
21553
|
*/
|
|
21291
21554
|
function convertFromGooseFormat(extensions) {
|
|
21292
21555
|
const result = {};
|
|
21556
|
+
const skipped = [];
|
|
21293
21557
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
21294
21558
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
21559
|
+
const type = existingExtensionType(ext);
|
|
21560
|
+
if (type === void 0 || !GOOSE_MCP_EXTENSION_TYPES.has(type)) {
|
|
21561
|
+
skipped.push(name);
|
|
21562
|
+
continue;
|
|
21563
|
+
}
|
|
21295
21564
|
const server = {};
|
|
21296
|
-
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
21297
21565
|
if (type === "sse") server.type = "sse";
|
|
21298
21566
|
else if (type === "streamable_http") server.type = "http";
|
|
21299
|
-
else
|
|
21567
|
+
else server.type = "stdio";
|
|
21300
21568
|
if (typeof ext.cmd === "string") server.command = ext.cmd;
|
|
21301
21569
|
if (isStringArray$1(ext.args)) server.args = ext.args;
|
|
21302
21570
|
if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
|
|
@@ -21306,6 +21574,7 @@ function convertFromGooseFormat(extensions) {
|
|
|
21306
21574
|
if (typeof ext.timeout === "number") server.timeout = ext.timeout;
|
|
21307
21575
|
result[name] = server;
|
|
21308
21576
|
}
|
|
21577
|
+
if (skipped.length > 0) warnWithFallback(void 0, `Skipping ${skipped.length} non-MCP Goose extension(s) (${skipped.map((name) => `"${name}"`).join(", ")}): they describe capabilities Goose provides itself and have no rulesync representation.`);
|
|
21309
21578
|
return result;
|
|
21310
21579
|
}
|
|
21311
21580
|
/**
|
|
@@ -21427,15 +21696,25 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
21427
21696
|
global
|
|
21428
21697
|
});
|
|
21429
21698
|
}
|
|
21430
|
-
const
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
};
|
|
21699
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21700
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
21701
|
+
const config = parseGooseConfig(existingContent, paths.relativeDirPath, paths.relativeFilePath);
|
|
21702
|
+
const existingExtensions = isRecord(config.extensions) ? config.extensions : {};
|
|
21434
21703
|
return new GooseMcp({
|
|
21435
21704
|
outputRoot,
|
|
21436
21705
|
relativeDirPath: paths.relativeDirPath,
|
|
21437
21706
|
relativeFilePath: paths.relativeFilePath,
|
|
21438
|
-
fileContent: (
|
|
21707
|
+
fileContent: applySharedConfigPatch({
|
|
21708
|
+
fileKey: sharedConfigFileKey(paths),
|
|
21709
|
+
feature: "mcp",
|
|
21710
|
+
existingContent,
|
|
21711
|
+
patch: { extensions: convertToGooseFormat({
|
|
21712
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
21713
|
+
existingExtensions,
|
|
21714
|
+
logger
|
|
21715
|
+
}) },
|
|
21716
|
+
filePath
|
|
21717
|
+
}),
|
|
21439
21718
|
validate,
|
|
21440
21719
|
global
|
|
21441
21720
|
});
|
|
@@ -28144,7 +28423,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
|
|
|
28144
28423
|
"interactionMode",
|
|
28145
28424
|
"extraKnownMarketplaces",
|
|
28146
28425
|
"enabledPlugins",
|
|
28147
|
-
"hooksDisabled"
|
|
28426
|
+
"hooksDisabled",
|
|
28427
|
+
"disabledSkills"
|
|
28148
28428
|
];
|
|
28149
28429
|
/**
|
|
28150
28430
|
* Permissions adapter for Factory Droid.
|
|
@@ -32295,6 +32575,30 @@ const CANONICAL_TO_ZED_TOOL_NAMES = {
|
|
|
32295
32575
|
webfetch: "fetch",
|
|
32296
32576
|
websearch: "search_web"
|
|
32297
32577
|
};
|
|
32578
|
+
/**
|
|
32579
|
+
* Canonical categories whose Zed tool is not permission-gated. Zed's gated list
|
|
32580
|
+
* is `terminal`, `edit_file`, `write_file`, `delete_path`, `move_path`,
|
|
32581
|
+
* `copy_path`, `create_directory`, `fetch`, `search_web` and `skill`; the
|
|
32582
|
+
* read-only tools (`read_file`, `grep`, `find_path`, `list_directory`) sit in
|
|
32583
|
+
* Zed's own `EXCLUDED_TOOLS` and never call `decide_permission_from_settings`,
|
|
32584
|
+
* so a `tools.<name>` entry for one is config Zed never consults. Zed's real
|
|
32585
|
+
* read-denial surface is `private_files`, which the ignore feature owns.
|
|
32586
|
+
*
|
|
32587
|
+
* @see https://zed.dev/docs/ai/tool-permissions#supported-tools
|
|
32588
|
+
*/
|
|
32589
|
+
const ZED_EXCLUDED_CANONICAL_CATEGORIES = /* @__PURE__ */ new Set([
|
|
32590
|
+
"read",
|
|
32591
|
+
"grep",
|
|
32592
|
+
"glob"
|
|
32593
|
+
]);
|
|
32594
|
+
/** The Zed-side spellings of the same tools, for a category that names one directly. */
|
|
32595
|
+
const ZED_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
32596
|
+
"read_file",
|
|
32597
|
+
"grep",
|
|
32598
|
+
"find_path",
|
|
32599
|
+
"list_directory"
|
|
32600
|
+
]);
|
|
32601
|
+
const isZedExcludedCategory = (category) => ZED_EXCLUDED_CANONICAL_CATEGORIES.has(category) || ZED_EXCLUDED_TOOL_NAMES.has(toZedToolName(category));
|
|
32298
32602
|
const ZED_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZED_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
32299
32603
|
function toZedToolName(canonical) {
|
|
32300
32604
|
return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
|
|
@@ -32416,15 +32720,21 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
32416
32720
|
const existingTools = asRecord(toolPermissions.tools);
|
|
32417
32721
|
let managedDefault;
|
|
32418
32722
|
const managedTools = {};
|
|
32723
|
+
const excludedCategories = [];
|
|
32419
32724
|
for (const [category, rules] of Object.entries(config.permission)) {
|
|
32420
32725
|
if (category === "*") {
|
|
32421
32726
|
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
32422
32727
|
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.`);
|
|
32423
32728
|
continue;
|
|
32424
32729
|
}
|
|
32730
|
+
if (isZedExcludedCategory(category)) {
|
|
32731
|
+
if (Object.values(rules).some((action) => action === "deny" || action === "ask")) excludedCategories.push(category);
|
|
32732
|
+
continue;
|
|
32733
|
+
}
|
|
32425
32734
|
const tool = buildZedToolPermission(rules);
|
|
32426
32735
|
if (tool) managedTools[toZedToolName(category)] = tool;
|
|
32427
32736
|
}
|
|
32737
|
+
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\`.`);
|
|
32428
32738
|
const managedToolNames = new Set(Object.keys(managedTools));
|
|
32429
32739
|
if ("*" in config.permission) managedToolNames.add("*");
|
|
32430
32740
|
const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
|
|
@@ -33216,18 +33526,16 @@ var DirFeatureProcessor = class {
|
|
|
33216
33526
|
})) dirHasChanges = true;
|
|
33217
33527
|
}
|
|
33218
33528
|
const otherFiles = aiDir.getOtherFiles();
|
|
33219
|
-
const otherFileContents = [];
|
|
33220
33529
|
for (const file of otherFiles) {
|
|
33221
|
-
|
|
33222
|
-
|
|
33223
|
-
|
|
33224
|
-
|
|
33225
|
-
|
|
33226
|
-
|
|
33227
|
-
|
|
33228
|
-
|
|
33229
|
-
|
|
33230
|
-
}
|
|
33530
|
+
if (dirHasChanges) break;
|
|
33531
|
+
const filePath = (0, node_path.join)(dirPath, file.relativeFilePathToDirPath);
|
|
33532
|
+
const existingBuffer = await readFileBufferOrNull(filePath);
|
|
33533
|
+
if (!companionFileContentsEquivalent({
|
|
33534
|
+
filePath,
|
|
33535
|
+
expected: file.fileBuffer,
|
|
33536
|
+
existing: existingBuffer,
|
|
33537
|
+
composed: file.composed
|
|
33538
|
+
})) dirHasChanges = true;
|
|
33231
33539
|
}
|
|
33232
33540
|
if (!dirHasChanges) continue;
|
|
33233
33541
|
const relativeDir = aiDir.getRelativePathFromCwd();
|
|
@@ -33247,11 +33555,8 @@ var DirFeatureProcessor = class {
|
|
|
33247
33555
|
await writeFileContent((0, node_path.join)(dirPath, mainFile.name), mainFileContent);
|
|
33248
33556
|
changedPaths.push((0, node_path.join)(relativeDir, mainFile.name));
|
|
33249
33557
|
}
|
|
33250
|
-
for (const
|
|
33251
|
-
|
|
33252
|
-
const content = otherFileContents[i];
|
|
33253
|
-
if (content === void 0) throw new Error(`Internal error: content for file ${file.relativeFilePathToDirPath} is undefined. This indicates a synchronization issue between otherFiles and otherFileContents arrays.`);
|
|
33254
|
-
await writeFileContent(filePath, content);
|
|
33558
|
+
for (const file of otherFiles) {
|
|
33559
|
+
await writeFileBuffer((0, node_path.join)(dirPath, file.relativeFilePathToDirPath), file.fileBuffer);
|
|
33255
33560
|
changedPaths.push((0, node_path.join)(relativeDir, file.relativeFilePathToDirPath));
|
|
33256
33561
|
}
|
|
33257
33562
|
}
|
|
@@ -33977,6 +34282,39 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
33977
34282
|
return frontmatter;
|
|
33978
34283
|
}
|
|
33979
34284
|
/**
|
|
34285
|
+
* Escapes the glob metacharacters in a directory path so it matches literally.
|
|
34286
|
+
* A real directory name may contain them — `app/[slug]` in a Next.js tree is
|
|
34287
|
+
* the common case, and unescaped `[slug]` reads as a bracket expression that
|
|
34288
|
+
* matches a different subtree (or nothing at all).
|
|
34289
|
+
*
|
|
34290
|
+
* @see https://code.claude.com/docs/en/memory
|
|
34291
|
+
*/
|
|
34292
|
+
function escapeGlobLiteral(dirPath) {
|
|
34293
|
+
return dirPath.replaceAll(/[\\*?[\]{}()!]/g, "\\$&");
|
|
34294
|
+
}
|
|
34295
|
+
/**
|
|
34296
|
+
* Claude Code scopes a nested skill by its location: a skill living in
|
|
34297
|
+
* `apps/web/.claude/skills/deploy` only activates while working under
|
|
34298
|
+
* `apps/web`. rulesync generates every imported skill into the project-root
|
|
34299
|
+
* `.claude/skills/`, so on import that location-based scoping has to be
|
|
34300
|
+
* re-expressed as an explicit `paths` glob — otherwise the round-trip silently
|
|
34301
|
+
* promotes a subtree skill to global activation.
|
|
34302
|
+
*
|
|
34303
|
+
* Returns the derived glob for a nested discovery root, or `undefined` for the
|
|
34304
|
+
* project-root `.claude/skills` (and for any root whose subtree cannot be
|
|
34305
|
+
* determined), where no scoping is implied.
|
|
34306
|
+
*
|
|
34307
|
+
* @see https://code.claude.com/docs/en/skills
|
|
34308
|
+
*/
|
|
34309
|
+
function deriveNestedSkillPaths(relativeDirPath) {
|
|
34310
|
+
const posixDirPath = toPosixPath(relativeDirPath);
|
|
34311
|
+
const skillsDirSuffix = `/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`;
|
|
34312
|
+
if (!posixDirPath.endsWith(skillsDirSuffix)) return;
|
|
34313
|
+
const subtree = posixDirPath.slice(0, -skillsDirSuffix.length);
|
|
34314
|
+
if (subtree === "" || subtree === ".") return;
|
|
34315
|
+
return [`${escapeGlobLiteral(subtree)}/**`];
|
|
34316
|
+
}
|
|
34317
|
+
/**
|
|
33980
34318
|
* Represents a Claude Code skill directory.
|
|
33981
34319
|
* Unlike subagents and commands, skills are directories containing SKILL.md and other files.
|
|
33982
34320
|
* Extends ToolSkill to inherit directory management and security features from AiDir.
|
|
@@ -34064,6 +34402,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34064
34402
|
}
|
|
34065
34403
|
toRulesyncSkill() {
|
|
34066
34404
|
const frontmatter = this.getFrontmatter();
|
|
34405
|
+
const resolvedPaths = frontmatter.paths !== void 0 ? frontmatter.paths : deriveNestedSkillPaths(this.relativeDirPath);
|
|
34067
34406
|
const claudecodeSection = {
|
|
34068
34407
|
...frontmatter.when_to_use && { when_to_use: frontmatter.when_to_use },
|
|
34069
34408
|
...frontmatter["allowed-tools"] && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
@@ -34080,7 +34419,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34080
34419
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34081
34420
|
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34082
34421
|
...this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true },
|
|
34083
|
-
...
|
|
34422
|
+
...resolvedPaths !== void 0 && { paths: resolvedPaths }
|
|
34084
34423
|
};
|
|
34085
34424
|
const rulesyncFrontmatter = {
|
|
34086
34425
|
name: frontmatter.name,
|
|
@@ -34478,7 +34817,8 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
34478
34817
|
fileBuffer: Buffer.from((0, js_yaml.dump)(openaiObject, {
|
|
34479
34818
|
lineWidth: -1,
|
|
34480
34819
|
noRefs: true
|
|
34481
|
-
}))
|
|
34820
|
+
})),
|
|
34821
|
+
composed: true
|
|
34482
34822
|
}] : baseOtherFiles;
|
|
34483
34823
|
return new CodexCliSkill({
|
|
34484
34824
|
outputRoot,
|
|
@@ -34538,7 +34878,11 @@ const CopilotSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
34538
34878
|
name: zod_mini.z.string(),
|
|
34539
34879
|
description: zod_mini.z.string(),
|
|
34540
34880
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
34541
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
34881
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
34882
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string()),
|
|
34883
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
34884
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
34885
|
+
context: zod_mini.z.optional(zod_mini.z.string())
|
|
34542
34886
|
});
|
|
34543
34887
|
/**
|
|
34544
34888
|
* Represents a GitHub Copilot skill directory.
|
|
@@ -34596,7 +34940,11 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34596
34940
|
const frontmatter = this.getFrontmatter();
|
|
34597
34941
|
const copilotSection = {
|
|
34598
34942
|
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
34599
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
34943
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
34944
|
+
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
|
|
34945
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34946
|
+
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34947
|
+
...frontmatter.context !== void 0 && { context: frontmatter.context }
|
|
34600
34948
|
};
|
|
34601
34949
|
const rulesyncFrontmatter = {
|
|
34602
34950
|
name: frontmatter.name,
|
|
@@ -34618,11 +34966,24 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
34618
34966
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34619
34967
|
const settablePaths = CopilotSkill.getSettablePaths({ global });
|
|
34620
34968
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
34969
|
+
const copilotSection = rulesyncFrontmatter.copilot;
|
|
34970
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
34971
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
34972
|
+
section: copilotSection
|
|
34973
|
+
});
|
|
34974
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
34975
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
34976
|
+
section: copilotSection
|
|
34977
|
+
});
|
|
34621
34978
|
const copilotFrontmatter = {
|
|
34622
34979
|
name: rulesyncFrontmatter.name,
|
|
34623
34980
|
description: rulesyncFrontmatter.description,
|
|
34624
|
-
...
|
|
34625
|
-
...
|
|
34981
|
+
...copilotSection?.license !== void 0 && { license: copilotSection.license },
|
|
34982
|
+
...copilotSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotSection["allowed-tools"] },
|
|
34983
|
+
...copilotSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotSection["argument-hint"] },
|
|
34984
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
34985
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
34986
|
+
...copilotSection?.context !== void 0 && { context: copilotSection.context }
|
|
34626
34987
|
};
|
|
34627
34988
|
return new CopilotSkill({
|
|
34628
34989
|
outputRoot,
|
|
@@ -34769,14 +35130,23 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
34769
35130
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
34770
35131
|
const settablePaths = CopilotcliSkill.getSettablePaths({ global });
|
|
34771
35132
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35133
|
+
const copilotcliSection = rulesyncFrontmatter.copilotcli;
|
|
35134
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
35135
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35136
|
+
section: copilotcliSection
|
|
35137
|
+
});
|
|
35138
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35139
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
35140
|
+
section: copilotcliSection
|
|
35141
|
+
});
|
|
34772
35142
|
const copilotcliFrontmatter = {
|
|
34773
35143
|
name: rulesyncFrontmatter.name,
|
|
34774
35144
|
description: rulesyncFrontmatter.description,
|
|
34775
|
-
...
|
|
34776
|
-
...
|
|
34777
|
-
...
|
|
34778
|
-
...
|
|
34779
|
-
...
|
|
35145
|
+
...copilotcliSection?.license !== void 0 && { license: copilotcliSection.license },
|
|
35146
|
+
...copilotcliSection?.["allowed-tools"] !== void 0 && { "allowed-tools": copilotcliSection["allowed-tools"] },
|
|
35147
|
+
...copilotcliSection?.["argument-hint"] !== void 0 && { "argument-hint": copilotcliSection["argument-hint"] },
|
|
35148
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35149
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
34780
35150
|
};
|
|
34781
35151
|
return new CopilotcliSkill({
|
|
34782
35152
|
outputRoot,
|
|
@@ -35334,7 +35704,9 @@ const FactorydroidSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
35334
35704
|
name: zod_mini.z.string(),
|
|
35335
35705
|
description: zod_mini.z.string(),
|
|
35336
35706
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
35337
|
-
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
|
|
35707
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
35708
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
35709
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
35338
35710
|
});
|
|
35339
35711
|
/**
|
|
35340
35712
|
* Represents a Factory Droid skill directory.
|
|
@@ -35390,7 +35762,9 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35390
35762
|
const frontmatter = this.getFrontmatter();
|
|
35391
35763
|
const factorydroidBlock = {
|
|
35392
35764
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
35393
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] }
|
|
35765
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
35766
|
+
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
35767
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
35394
35768
|
};
|
|
35395
35769
|
const rulesyncFrontmatter = {
|
|
35396
35770
|
name: frontmatter.name,
|
|
@@ -35412,19 +35786,22 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
35412
35786
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
35413
35787
|
const settablePaths = FactorydroidSkill.getSettablePaths({ global });
|
|
35414
35788
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
35789
|
+
const factorydroidSection = rulesyncFrontmatter.factorydroid;
|
|
35415
35790
|
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
35416
35791
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35417
|
-
section:
|
|
35792
|
+
section: factorydroidSection
|
|
35418
35793
|
});
|
|
35419
35794
|
const resolvedUserInvocable = resolveUserInvocable({
|
|
35420
35795
|
rootFrontmatter: rulesyncFrontmatter,
|
|
35421
|
-
section:
|
|
35796
|
+
section: factorydroidSection
|
|
35422
35797
|
});
|
|
35423
35798
|
const factorydroidFrontmatter = {
|
|
35424
35799
|
name: rulesyncFrontmatter.name,
|
|
35425
35800
|
description: rulesyncFrontmatter.description,
|
|
35426
35801
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
35427
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
35802
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
35803
|
+
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
35804
|
+
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
35428
35805
|
};
|
|
35429
35806
|
return new FactorydroidSkill({
|
|
35430
35807
|
outputRoot,
|
|
@@ -39445,7 +39822,17 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39445
39822
|
validate: true
|
|
39446
39823
|
});
|
|
39447
39824
|
}
|
|
39448
|
-
|
|
39825
|
+
/**
|
|
39826
|
+
* Last chance to adjust the tool frontmatter before it is written. The base
|
|
39827
|
+
* implementation only warns about names Claude Code rejects; plugin-scoped
|
|
39828
|
+
* subclasses extend it to drop fields Claude Code refuses to honor for
|
|
39829
|
+
* plugin-shipped agents.
|
|
39830
|
+
*/
|
|
39831
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
39832
|
+
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.`);
|
|
39833
|
+
return frontmatter;
|
|
39834
|
+
}
|
|
39835
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
39449
39836
|
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
39450
39837
|
const claudecodeSection = this.filterToolSpecificSection(rulesyncFrontmatter.claudecode ?? {}, ["name", "description"]);
|
|
39451
39838
|
const rawClaudecodeFrontmatter = {
|
|
@@ -39455,7 +39842,11 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39455
39842
|
};
|
|
39456
39843
|
const result = ClaudecodeSubagentFrontmatterSchema.safeParse(rawClaudecodeFrontmatter);
|
|
39457
39844
|
if (!result.success) throw new Error(`Invalid claudecode subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
39458
|
-
const claudecodeFrontmatter =
|
|
39845
|
+
const claudecodeFrontmatter = this.sanitizeFrontmatter({
|
|
39846
|
+
frontmatter: result.data,
|
|
39847
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
39848
|
+
logger
|
|
39849
|
+
});
|
|
39459
39850
|
const body = rulesyncSubagent.getBody();
|
|
39460
39851
|
const fileContent = stringifyFrontmatter(body, claudecodeFrontmatter);
|
|
39461
39852
|
const paths = this.getSettablePaths({ global });
|
|
@@ -39524,6 +39915,21 @@ var ClaudecodeSubagent = class ClaudecodeSubagent extends ToolSubagent {
|
|
|
39524
39915
|
};
|
|
39525
39916
|
//#endregion
|
|
39526
39917
|
//#region src/features/subagents/claudecode-plugin-subagent.ts
|
|
39918
|
+
/**
|
|
39919
|
+
* Claude Code refuses these for plugin-shipped agents "for security reasons",
|
|
39920
|
+
* so emitting them leaves the author believing the agent is constrained when it
|
|
39921
|
+
* is not. Only these three are dropped: the other fields upstream does not list
|
|
39922
|
+
* (e.g. `color`) are merely ignored, with no misleading security posture.
|
|
39923
|
+
*
|
|
39924
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
39925
|
+
*/
|
|
39926
|
+
const PLUGIN_FORBIDDEN_FIELDS = [
|
|
39927
|
+
"hooks",
|
|
39928
|
+
"mcpServers",
|
|
39929
|
+
"permissionMode"
|
|
39930
|
+
];
|
|
39931
|
+
/** The only `isolation` value plugin agents accept. */
|
|
39932
|
+
const PLUGIN_ISOLATION_VALUE = "worktree";
|
|
39527
39933
|
var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
39528
39934
|
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
39529
39935
|
const targets = rulesyncSubagent.getFrontmatter().targets;
|
|
@@ -39532,6 +39938,21 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
|
39532
39938
|
static getSettablePaths() {
|
|
39533
39939
|
return { relativeDirPath: CLAUDECODE_PLUGIN_AGENTS_DIR };
|
|
39534
39940
|
}
|
|
39941
|
+
static sanitizeFrontmatter({ frontmatter, relativeFilePath, logger }) {
|
|
39942
|
+
const sanitized = { ...super.sanitizeFrontmatter({
|
|
39943
|
+
frontmatter,
|
|
39944
|
+
relativeFilePath,
|
|
39945
|
+
logger
|
|
39946
|
+
}) };
|
|
39947
|
+
const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
|
|
39948
|
+
for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
|
|
39949
|
+
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.`);
|
|
39950
|
+
if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
|
|
39951
|
+
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.`);
|
|
39952
|
+
delete sanitized.isolation;
|
|
39953
|
+
}
|
|
39954
|
+
return sanitized;
|
|
39955
|
+
}
|
|
39535
39956
|
};
|
|
39536
39957
|
//#endregion
|
|
39537
39958
|
//#region src/features/subagents/cline-subagent.ts
|
|
@@ -43185,7 +43606,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
43185
43606
|
outputRoot: this.outputRoot,
|
|
43186
43607
|
relativeDirPath: RulesyncSubagent.getSettablePaths().relativeDirPath,
|
|
43187
43608
|
rulesyncSubagent,
|
|
43188
|
-
global: this.global
|
|
43609
|
+
global: this.global,
|
|
43610
|
+
logger: this.logger
|
|
43189
43611
|
}));
|
|
43190
43612
|
}
|
|
43191
43613
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
@@ -45138,7 +45560,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
|
45138
45560
|
};
|
|
45139
45561
|
//#endregion
|
|
45140
45562
|
//#region src/features/rules/copilot-rule.ts
|
|
45141
|
-
const CopilotRuleFrontmatterSchema = zod_mini.z.
|
|
45563
|
+
const CopilotRuleFrontmatterSchema = zod_mini.z.looseObject({
|
|
45142
45564
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
45143
45565
|
applyTo: zod_mini.z.optional(zod_mini.z.string()),
|
|
45144
45566
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -45194,15 +45616,13 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45194
45616
|
toRulesyncRule() {
|
|
45195
45617
|
let globs;
|
|
45196
45618
|
if (this.frontmatter.applyTo) globs = this.frontmatter.applyTo.split(",").map((g) => g.trim());
|
|
45619
|
+
const { description, applyTo: _applyTo, ...copilotFields } = this.frontmatter;
|
|
45197
45620
|
const rulesyncFrontmatter = {
|
|
45198
45621
|
targets: ["*"],
|
|
45199
45622
|
root: this.isRoot(),
|
|
45200
|
-
description
|
|
45623
|
+
description,
|
|
45201
45624
|
globs,
|
|
45202
|
-
...(
|
|
45203
|
-
...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
|
|
45204
|
-
...this.frontmatter.name && { name: this.frontmatter.name }
|
|
45205
|
-
} }
|
|
45625
|
+
...Object.keys(copilotFields).length > 0 && { copilot: copilotFields }
|
|
45206
45626
|
};
|
|
45207
45627
|
const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
|
|
45208
45628
|
return new RulesyncRule({
|
|
@@ -45219,10 +45639,9 @@ var CopilotRule = class CopilotRule extends ToolRule {
|
|
|
45219
45639
|
const root = rulesyncFrontmatter.root;
|
|
45220
45640
|
const paths = this.getSettablePaths({ global });
|
|
45221
45641
|
const copilotFrontmatter = {
|
|
45642
|
+
...rulesyncFrontmatter.copilot,
|
|
45222
45643
|
description: rulesyncFrontmatter.description,
|
|
45223
|
-
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45224
|
-
excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
|
|
45225
|
-
name: rulesyncFrontmatter.copilot?.name
|
|
45644
|
+
applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0
|
|
45226
45645
|
};
|
|
45227
45646
|
const body = rulesyncRule.getBody();
|
|
45228
45647
|
if (root) return new CopilotRule({
|
|
@@ -51621,6 +52040,12 @@ Object.defineProperty(exports, "warnOnConflictingFlags", {
|
|
|
51621
52040
|
return warnOnConflictingFlags;
|
|
51622
52041
|
}
|
|
51623
52042
|
});
|
|
52043
|
+
Object.defineProperty(exports, "writeFileBuffer", {
|
|
52044
|
+
enumerable: true,
|
|
52045
|
+
get: function() {
|
|
52046
|
+
return writeFileBuffer;
|
|
52047
|
+
}
|
|
52048
|
+
});
|
|
51624
52049
|
Object.defineProperty(exports, "writeFileContent", {
|
|
51625
52050
|
enumerable: true,
|
|
51626
52051
|
get: function() {
|