rulesync 32.0.0 → 34.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli/index.cjs +429 -127
- package/dist/cli/index.js +429 -127
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-Bmb7ofrF.cjs → import-Bx0zNGBx.cjs} +804 -145
- package/dist/{import-DgKWnDni.js → import-DFwX4TTE.js} +777 -130
- package/dist/{import-DgKWnDni.js.map → import-DFwX4TTE.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +15 -15
|
@@ -386,7 +386,11 @@ const DeprecatedIgnoreFeatureSchema = z.literal("ignore").check(meta({
|
|
|
386
386
|
const FeatureSchema = z.union([z.enum(ACTIVE_FEATURES), DeprecatedIgnoreFeatureSchema]);
|
|
387
387
|
z.array(FeatureSchema);
|
|
388
388
|
const FeatureWithWildcardSchema = z.union([z.enum([...ACTIVE_FEATURES, "*"]), DeprecatedIgnoreFeatureSchema]);
|
|
389
|
-
const GitignoreDestinationSchema = z.enum([
|
|
389
|
+
const GitignoreDestinationSchema = z.enum([
|
|
390
|
+
"gitignore",
|
|
391
|
+
"gitattributes",
|
|
392
|
+
"gitexclude"
|
|
393
|
+
]);
|
|
390
394
|
const FlattenedCommandNamingSchema = z.enum(["basename", "path"]);
|
|
391
395
|
const FeatureOptionsSchema = z.record(z.string(), z.unknown());
|
|
392
396
|
const FeatureValueSchema = z.union([
|
|
@@ -506,6 +510,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
506
510
|
"claudecode-legacy",
|
|
507
511
|
"cline",
|
|
508
512
|
"codebuddy",
|
|
513
|
+
"codebuff",
|
|
509
514
|
"codewhale",
|
|
510
515
|
"codexcli",
|
|
511
516
|
"commandcode",
|
|
@@ -1385,6 +1390,13 @@ async function getFileSize(filepath) {
|
|
|
1385
1390
|
throw new Error(`Failed to get file size for "${filepath}": ${formatError(error)}`, { cause: error });
|
|
1386
1391
|
}
|
|
1387
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Whether a file carries any execute bit. Windows reports none, so a file
|
|
1395
|
+
* there is never seen as executable.
|
|
1396
|
+
*/
|
|
1397
|
+
async function hasExecutableBit(filepath) {
|
|
1398
|
+
return ((await stat(filepath)).mode & 73) !== 0;
|
|
1399
|
+
}
|
|
1388
1400
|
async function isSymlink(filepath) {
|
|
1389
1401
|
try {
|
|
1390
1402
|
return (await lstat(filepath)).isSymbolicLink();
|
|
@@ -3157,6 +3169,9 @@ var Config = class Config {
|
|
|
3157
3169
|
const featureValue = value[feature];
|
|
3158
3170
|
if (featureValue && typeof featureValue === "object" && isFeatureValueEnabled(featureValue)) return featureValue;
|
|
3159
3171
|
}
|
|
3172
|
+
getRootGitignoreDestination() {
|
|
3173
|
+
return this.gitignoreDestination;
|
|
3174
|
+
}
|
|
3160
3175
|
getGitignoreDestination(target, feature) {
|
|
3161
3176
|
const rootLevel = this.gitignoreDestination;
|
|
3162
3177
|
if (!isRulesyncConfigTargetsObject(this.targets)) return rootLevel;
|
|
@@ -3174,7 +3189,8 @@ var Config = class Config {
|
|
|
3174
3189
|
return toolLevel ?? rootLevel;
|
|
3175
3190
|
}
|
|
3176
3191
|
static parseGitignoreDestination(value) {
|
|
3177
|
-
|
|
3192
|
+
const result = GitignoreDestinationSchema.safeParse(value);
|
|
3193
|
+
return result.success ? result.data : void 0;
|
|
3178
3194
|
}
|
|
3179
3195
|
/**
|
|
3180
3196
|
* Check if per-target features configuration is being used.
|
|
@@ -5244,12 +5260,20 @@ const JUNIE_HOOK_EVENTS = [
|
|
|
5244
5260
|
"sessionEnd"
|
|
5245
5261
|
];
|
|
5246
5262
|
/**
|
|
5247
|
-
* Hook events supported by the GitLab Duo CLI.
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
5263
|
+
* Hook events supported by the GitLab Duo CLI. `SessionStart` is documented;
|
|
5264
|
+
* `PreToolUse` and `SessionEnd` ship in gitlab-lsp (`HOOK_EVENT_NAMES`, v9.28.0+)
|
|
5265
|
+
* but are not on docs.gitlab.com yet. The optional regex matcher filters on the
|
|
5266
|
+
* session source (`startup`, `resume`) for `SessionStart`, on `tool_name` for
|
|
5267
|
+
* `PreToolUse`, and on the end reason for `SessionEnd`. Only `type: "command"`
|
|
5268
|
+
* hooks are supported.
|
|
5250
5269
|
* @see https://docs.gitlab.com/user/gitlab_duo_cli/customize/
|
|
5270
|
+
* @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/core/hooks/src/schema.ts
|
|
5251
5271
|
*/
|
|
5252
|
-
const GITLABDUO_HOOK_EVENTS = [
|
|
5272
|
+
const GITLABDUO_HOOK_EVENTS = [
|
|
5273
|
+
"sessionStart",
|
|
5274
|
+
"preToolUse",
|
|
5275
|
+
"sessionEnd"
|
|
5276
|
+
];
|
|
5253
5277
|
/**
|
|
5254
5278
|
* Hook events supported by Qwen Code.
|
|
5255
5279
|
*
|
|
@@ -5590,7 +5614,10 @@ const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
|
|
|
5590
5614
|
* or `agent` hooks with a `timeout` in seconds. The hooks reference documents ten events; the
|
|
5591
5615
|
* plugins reference lists the full set, stating that plugin hooks "respond to
|
|
5592
5616
|
* the same lifecycle events as user-defined hooks", so every event listed
|
|
5593
|
-
* there is supported here.
|
|
5617
|
+
* there is supported here. The hooks reference also names `Setup`
|
|
5618
|
+
* (bootstrap/maintenance), and the published package's settings schema
|
|
5619
|
+
* (`@tencent-ai/codebuddy-code` 2.167.0) accepts `UserPromptExpansion`, whose
|
|
5620
|
+
* matcher filters on `command_name` as in Claude Code.
|
|
5594
5621
|
*
|
|
5595
5622
|
* @see https://www.codebuddy.ai/docs/cli/hooks
|
|
5596
5623
|
* @see https://www.codebuddy.ai/docs/cli/plugins-reference
|
|
@@ -5621,7 +5648,9 @@ const CODEBUDDY_HOOK_EVENTS = [
|
|
|
5621
5648
|
"postCompact",
|
|
5622
5649
|
"elicitation",
|
|
5623
5650
|
"elicitationResult",
|
|
5624
|
-
"sessionEnd"
|
|
5651
|
+
"sessionEnd",
|
|
5652
|
+
"setup",
|
|
5653
|
+
"userPromptExpansion"
|
|
5625
5654
|
];
|
|
5626
5655
|
/**
|
|
5627
5656
|
* CodeBuddy Code events that fire on every occurrence and take no `matcher`.
|
|
@@ -5670,7 +5699,9 @@ const CANONICAL_TO_CODEBUDDY_EVENT_NAMES = {
|
|
|
5670
5699
|
postCompact: "PostCompact",
|
|
5671
5700
|
elicitation: "Elicitation",
|
|
5672
5701
|
elicitationResult: "ElicitationResult",
|
|
5673
|
-
sessionEnd: "SessionEnd"
|
|
5702
|
+
sessionEnd: "SessionEnd",
|
|
5703
|
+
setup: "Setup",
|
|
5704
|
+
userPromptExpansion: "UserPromptExpansion"
|
|
5674
5705
|
};
|
|
5675
5706
|
const CODEBUDDY_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CODEBUDDY_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5676
5707
|
/**
|
|
@@ -6524,7 +6555,11 @@ const KIRO_IDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
|
|
|
6524
6555
|
/**
|
|
6525
6556
|
* Map canonical camelCase event names to GitLab Duo CLI PascalCase.
|
|
6526
6557
|
*/
|
|
6527
|
-
const CANONICAL_TO_GITLABDUO_EVENT_NAMES = {
|
|
6558
|
+
const CANONICAL_TO_GITLABDUO_EVENT_NAMES = {
|
|
6559
|
+
sessionStart: "SessionStart",
|
|
6560
|
+
preToolUse: "PreToolUse",
|
|
6561
|
+
sessionEnd: "SessionEnd"
|
|
6562
|
+
};
|
|
6528
6563
|
/**
|
|
6529
6564
|
* Map GitLab Duo CLI PascalCase event names to canonical camelCase.
|
|
6530
6565
|
*/
|
|
@@ -8265,6 +8300,48 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
8265
8300
|
]))
|
|
8266
8301
|
});
|
|
8267
8302
|
/**
|
|
8303
|
+
* Tool-scoped override block for Qoder. `outputStyle` is written to the
|
|
8304
|
+
* top-level `outputStyle` key of `.qoder/settings.json` (project) /
|
|
8305
|
+
* `~/.qoder/settings.json` (global): the name of the active output style, a
|
|
8306
|
+
* custom style from `.qoder/output-styles/` (see the `qoder.channel:
|
|
8307
|
+
* output-style` rule option) or a `plugin-name:style-name` reference. The
|
|
8308
|
+
* shared `permission` block continues to drive `permissions.allow`/`ask`/`deny`.
|
|
8309
|
+
*
|
|
8310
|
+
* @see https://docs.qoder.com/cli/output-styles
|
|
8311
|
+
* @see https://docs.qoder.com/cli/settings-reference
|
|
8312
|
+
* @example
|
|
8313
|
+
* { "outputStyle": "concise-review" }
|
|
8314
|
+
*/
|
|
8315
|
+
const QoderPermissionsOverrideSchema = z.looseObject({
|
|
8316
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
8317
|
+
outputStyle: z.optional(z.string().check(z.trim(), z.minLength(1)))
|
|
8318
|
+
});
|
|
8319
|
+
/**
|
|
8320
|
+
* One native Factory Droid permission rule. `match.prefix` positions are either
|
|
8321
|
+
* a single token or a non-empty list of alternative tokens.
|
|
8322
|
+
*
|
|
8323
|
+
* @see https://docs.factory.com/autonomy-and-safety/permission-rules
|
|
8324
|
+
*/
|
|
8325
|
+
const FactorydroidPermissionRuleSchema = z.looseObject({
|
|
8326
|
+
id: z.string(),
|
|
8327
|
+
decision: z.enum([
|
|
8328
|
+
"allow",
|
|
8329
|
+
"ask",
|
|
8330
|
+
"block"
|
|
8331
|
+
]),
|
|
8332
|
+
match: z.looseObject({ prefix: z.optional(z.array(z.union([z.string(), z.array(z.string()).check(z.minLength(1))])).check(z.minLength(1))) }),
|
|
8333
|
+
tests: z.optional(z.looseObject({
|
|
8334
|
+
match: z.optional(z.array(z.string())),
|
|
8335
|
+
noMatch: z.optional(z.array(z.string()))
|
|
8336
|
+
})),
|
|
8337
|
+
reason: z.optional(z.string()),
|
|
8338
|
+
enabled: z.optional(z.boolean())
|
|
8339
|
+
});
|
|
8340
|
+
const FactorydroidPermissionRulesSchema = z.looseObject({
|
|
8341
|
+
version: z.number(),
|
|
8342
|
+
rules: z.array(FactorydroidPermissionRuleSchema)
|
|
8343
|
+
});
|
|
8344
|
+
/**
|
|
8268
8345
|
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
8269
8346
|
* exposes security controls with no canonical per-command allow/ask/deny slot —
|
|
8270
8347
|
* `commandBlocklist` (a hard-block tier that can never be approved, distinct from
|
|
@@ -8272,7 +8349,8 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
8272
8349
|
* (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`,
|
|
8273
8350
|
* `mcpAutonomyOverrides` (per-MCP-tool autonomy levels), `enableDroidShield`,
|
|
8274
8351
|
* and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
|
|
8275
|
-
* `subagentAutonomyLevel`, `interactionMode`)
|
|
8352
|
+
* `subagentAutonomyLevel`, `interactionMode`), and the native versioned
|
|
8353
|
+
* `permissionRules` (allow/ask/block). Fields placed here are merged
|
|
8276
8354
|
* into `settings.json` and
|
|
8277
8355
|
* emitted only for Factory Droid, while the shared `permission` block continues
|
|
8278
8356
|
* to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
|
|
@@ -8283,7 +8361,8 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
8283
8361
|
const FactorydroidPermissionsOverrideSchema = z.looseObject({
|
|
8284
8362
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
8285
8363
|
commandBlocklist: z.optional(z.array(z.string())),
|
|
8286
|
-
disabledSkills: z.optional(z.array(z.string()))
|
|
8364
|
+
disabledSkills: z.optional(z.array(z.string())),
|
|
8365
|
+
permissionRules: z.optional(FactorydroidPermissionRulesSchema)
|
|
8287
8366
|
});
|
|
8288
8367
|
/**
|
|
8289
8368
|
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
@@ -9088,6 +9167,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
9088
9167
|
codewhale: z.optional(CodewhalePermissionsOverrideSchema),
|
|
9089
9168
|
zed: z.optional(ZedPermissionsOverrideSchema),
|
|
9090
9169
|
devin: z.optional(DevinPermissionsOverrideSchema),
|
|
9170
|
+
qoder: z.optional(QoderPermissionsOverrideSchema),
|
|
9091
9171
|
"antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
|
|
9092
9172
|
continue: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9093
9173
|
copilot: z.optional(CopilotPermissionsOverrideSchema),
|
|
@@ -9099,7 +9179,6 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
9099
9179
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9100
9180
|
omp: z.optional(OmpPermissionsOverrideSchema),
|
|
9101
9181
|
pool: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9102
|
-
qoder: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9103
9182
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9104
9183
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
9105
9184
|
roo: z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -9620,7 +9699,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
9620
9699
|
qoder: z.optional(z.looseObject({
|
|
9621
9700
|
trigger: z.optional(z.string()),
|
|
9622
9701
|
glob: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
9623
|
-
description: z.optional(z.string())
|
|
9702
|
+
description: z.optional(z.string()),
|
|
9703
|
+
channel: z.optional(z.enum(["output-style"])),
|
|
9704
|
+
name: z.optional(z.string().check(z.trim(), z.minLength(1)))
|
|
9624
9705
|
})),
|
|
9625
9706
|
augmentcode: z.optional(z.looseObject({
|
|
9626
9707
|
type: z.optional(z.string()),
|
|
@@ -12989,7 +13070,7 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12989
13070
|
},
|
|
12990
13071
|
permissions: {
|
|
12991
13072
|
kind: "replace-owned-keys",
|
|
12992
|
-
ownedKeys: ["permissions"]
|
|
13073
|
+
ownedKeys: ["permissions", "outputStyle"]
|
|
12993
13074
|
}
|
|
12994
13075
|
}
|
|
12995
13076
|
},
|
|
@@ -21744,22 +21825,29 @@ function mergeOpencodeV2Entries({ config, legacyKey, pluralKey, lower }) {
|
|
|
21744
21825
|
const entry = asOpencodeEntries(value);
|
|
21745
21826
|
const lowered = entry === null ? null : lower(entry);
|
|
21746
21827
|
if (Object.hasOwn(merged, name) || lowered === null) continue;
|
|
21747
|
-
|
|
21748
|
-
value: lowered,
|
|
21749
|
-
enumerable: true,
|
|
21750
|
-
configurable: true,
|
|
21751
|
-
writable: true
|
|
21752
|
-
});
|
|
21828
|
+
setOwnEntry(merged, name, lowered);
|
|
21753
21829
|
}
|
|
21754
21830
|
return Object.keys(merged).length > 0 ? merged : legacy;
|
|
21755
21831
|
}
|
|
21756
21832
|
/**
|
|
21833
|
+
* Sets `target[key]` as an own, enumerable property. A plain assignment to a
|
|
21834
|
+
* key named `__proto__` would set the prototype instead.
|
|
21835
|
+
*/
|
|
21836
|
+
function setOwnEntry(target, key, value) {
|
|
21837
|
+
Object.defineProperty(target, key, {
|
|
21838
|
+
value,
|
|
21839
|
+
enumerable: true,
|
|
21840
|
+
configurable: true,
|
|
21841
|
+
writable: true
|
|
21842
|
+
});
|
|
21843
|
+
}
|
|
21844
|
+
/**
|
|
21757
21845
|
* Returns the inline agent entries of an OpenCode config in the V1 `agent`
|
|
21758
21846
|
* shape. Since v1.18.24 OpenCode V1 also reads the V2 `agents` spelling and
|
|
21759
21847
|
* lowers it, so a layout that does so (`readsV2ConfigSpellings`) gets those
|
|
21760
21848
|
* entries too.
|
|
21761
21849
|
*
|
|
21762
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21850
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21763
21851
|
*/
|
|
21764
21852
|
function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
21765
21853
|
if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.agent);
|
|
@@ -21778,7 +21866,7 @@ function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21778
21866
|
* Other fields are kept: V1 drops a V2 `subagent`, but rulesync reads it as
|
|
21779
21867
|
* `subtask` everywhere (as V2 does), so it is left for the command importer.
|
|
21780
21868
|
*
|
|
21781
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21869
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21782
21870
|
*/
|
|
21783
21871
|
function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
21784
21872
|
if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.command);
|
|
@@ -21801,7 +21889,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21801
21889
|
* reads V2 spellings, a flat V2 `skills` array is accepted too: its entries
|
|
21802
21890
|
* that are not `http(s)://` URLs are the paths, as OpenCode V1 lowers them.
|
|
21803
21891
|
*
|
|
21804
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21892
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21805
21893
|
*/
|
|
21806
21894
|
function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
|
|
21807
21895
|
if (Array.isArray(config.skills)) {
|
|
@@ -21811,6 +21899,160 @@ function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21811
21899
|
const skills = asOpencodeEntries(config.skills);
|
|
21812
21900
|
return skills !== null && Array.isArray(skills.paths) ? skills.paths : [];
|
|
21813
21901
|
}
|
|
21902
|
+
/**
|
|
21903
|
+
* Whether a record is itself an MCP server entry rather than a V2 container
|
|
21904
|
+
* (`mcp.servers`, `mcp.timeout`): a server — or a server literally named
|
|
21905
|
+
* `type` / `enabled` — carries a scalar `type` or `enabled`. Mirrors OpenCode
|
|
21906
|
+
* V1's `isDirectServer`.
|
|
21907
|
+
*/
|
|
21908
|
+
function isDirectOpencodeMcpServer(value) {
|
|
21909
|
+
return ["type", "enabled"].some((key) => Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])));
|
|
21910
|
+
}
|
|
21911
|
+
const OPENCODE_V2_TIMEOUT_KEYS = [
|
|
21912
|
+
"startup",
|
|
21913
|
+
"catalog",
|
|
21914
|
+
"execution"
|
|
21915
|
+
];
|
|
21916
|
+
/**
|
|
21917
|
+
* Whether an MCP server entry uses a V2-only per-server spelling, so that
|
|
21918
|
+
* OpenCode V1 lowers it: `disabled`, `codemode`, an object `timeout`, or a
|
|
21919
|
+
* snake_case `oauth` key.
|
|
21920
|
+
*/
|
|
21921
|
+
function isOpencodeV2NativeServer(value) {
|
|
21922
|
+
const oauth = value.oauth;
|
|
21923
|
+
return Object.hasOwn(value, "disabled") || Object.hasOwn(value, "codemode") || isRecord$1(value.timeout) || isRecord$1(oauth) && [
|
|
21924
|
+
"client_id",
|
|
21925
|
+
"client_secret",
|
|
21926
|
+
"callback_port",
|
|
21927
|
+
"redirect_uri"
|
|
21928
|
+
].some((key) => Object.hasOwn(oauth, key));
|
|
21929
|
+
}
|
|
21930
|
+
/**
|
|
21931
|
+
* Lowers a V2 timeout object (`{ startup, catalog, execution }`) to V1's single
|
|
21932
|
+
* number, the way OpenCode V1 does: only a `catalog` equal to `execution`, with
|
|
21933
|
+
* no `startup`, has a V1 equivalent. Anything else — including a value that is
|
|
21934
|
+
* not a positive integer — is `undefined` (dropped).
|
|
21935
|
+
*/
|
|
21936
|
+
function lowerOpencodeV2Timeout(value) {
|
|
21937
|
+
if (!hasPositiveIntegerTimeouts(value) || value.startup !== void 0 || typeof value.catalog !== "number") return;
|
|
21938
|
+
return value.catalog === value.execution ? value.catalog : void 0;
|
|
21939
|
+
}
|
|
21940
|
+
const OPENCODE_V2_OAUTH_KEYS = /* @__PURE__ */ new Map([
|
|
21941
|
+
["client_id", "clientId"],
|
|
21942
|
+
["client_secret", "clientSecret"],
|
|
21943
|
+
["callback_port", "callbackPort"],
|
|
21944
|
+
["redirect_uri", "redirectUri"]
|
|
21945
|
+
]);
|
|
21946
|
+
/**
|
|
21947
|
+
* V2 `oauth` keys that have no V1 spelling. OpenCode V1 drops them when it
|
|
21948
|
+
* lowers a server, so they are dropped (and reported) here too rather than
|
|
21949
|
+
* being written back as a snake_case key among camelCase ones.
|
|
21950
|
+
*/
|
|
21951
|
+
const OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS = /* @__PURE__ */ new Set(["auth_server_metadata_url"]);
|
|
21952
|
+
function lowerOpencodeV2OAuth(oauth, path, onDropped) {
|
|
21953
|
+
const lowered = {};
|
|
21954
|
+
for (const [key, value] of Object.entries(oauth)) {
|
|
21955
|
+
if (OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS.has(key)) {
|
|
21956
|
+
onDropped?.(`${path}.${key}`, "unsupported");
|
|
21957
|
+
continue;
|
|
21958
|
+
}
|
|
21959
|
+
setOwnEntry(lowered, OPENCODE_V2_OAUTH_KEYS.get(key) ?? key, value);
|
|
21960
|
+
}
|
|
21961
|
+
return lowered;
|
|
21962
|
+
}
|
|
21963
|
+
/**
|
|
21964
|
+
* Lowers one V2-spelled MCP server entry to V1's shape, as OpenCode V1 does:
|
|
21965
|
+
* `disabled` becomes `enabled` (an explicit `enabled` wins), an object
|
|
21966
|
+
* `timeout` becomes a number or is dropped, and snake_case `oauth` keys become
|
|
21967
|
+
* camelCase. `codemode` is kept: Rulesync passes it through where V1 drops it.
|
|
21968
|
+
*
|
|
21969
|
+
* Like V1, only an entry that is a local or remote server is lowered, and one
|
|
21970
|
+
* whose `enabled` or `disabled` is not a boolean is left as it is. Unlike V1,
|
|
21971
|
+
* such an entry is not dropped but passed on, so the schema rejects it loudly.
|
|
21972
|
+
*/
|
|
21973
|
+
function lowerOpencodeV2Server(value, path, onDropped) {
|
|
21974
|
+
if (!isRecord$1(value) || value.type !== "local" && value.type !== "remote" || Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean" || Object.hasOwn(value, "disabled") && typeof value.disabled !== "boolean") return value;
|
|
21975
|
+
const { disabled, timeout, ...rest } = value;
|
|
21976
|
+
const lowered = { ...rest };
|
|
21977
|
+
if (!Object.hasOwn(value, "enabled")) lowered.enabled = disabled !== true;
|
|
21978
|
+
else if (value.enabled === disabled) onDropped?.(`${path}.disabled`, "conflict");
|
|
21979
|
+
const lowTimeout = isRecord$1(timeout) ? lowerOpencodeV2Timeout(timeout) : timeout;
|
|
21980
|
+
if (lowTimeout !== void 0) lowered.timeout = lowTimeout;
|
|
21981
|
+
else if (isRecord$1(timeout) && Object.keys(timeout).length > 0) onDropped?.(`${path}.timeout`, "unsupported");
|
|
21982
|
+
if (isRecord$1(value.oauth)) lowered.oauth = lowerOpencodeV2OAuth(value.oauth, `${path}.oauth`, onDropped);
|
|
21983
|
+
return lowered;
|
|
21984
|
+
}
|
|
21985
|
+
function isPositiveInteger(value) {
|
|
21986
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
21987
|
+
}
|
|
21988
|
+
/** Whether each V2 timeout value a record carries is a positive integer. */
|
|
21989
|
+
function hasPositiveIntegerTimeouts(value) {
|
|
21990
|
+
return OPENCODE_V2_TIMEOUT_KEYS.every((key) => value[key] === void 0 || isPositiveInteger(value[key]));
|
|
21991
|
+
}
|
|
21992
|
+
/**
|
|
21993
|
+
* Whether `mcp.timeout` is V2's global timeout object rather than a server
|
|
21994
|
+
* named `timeout`. As in OpenCode V1, each of its values must be a positive
|
|
21995
|
+
* integer; anything else is read as a server, which the schema then rejects.
|
|
21996
|
+
*/
|
|
21997
|
+
function isOpencodeV2GlobalTimeout(value) {
|
|
21998
|
+
return isRecord$1(value) && !isDirectOpencodeMcpServer(value) && (Object.keys(value).length === 0 || OPENCODE_V2_TIMEOUT_KEYS.some((key) => Object.hasOwn(value, key))) && hasPositiveIntegerTimeouts(value);
|
|
21999
|
+
}
|
|
22000
|
+
function getOpencodeV2McpEnvelope(mcp) {
|
|
22001
|
+
const nested = mcp.servers;
|
|
22002
|
+
return isRecord$1(nested) && !isDirectOpencodeMcpServer(nested) ? nested : null;
|
|
22003
|
+
}
|
|
22004
|
+
/**
|
|
22005
|
+
* Returns the paths of the V2-only containers in an OpenCode `mcp` value that
|
|
22006
|
+
* a write in V1's flat shape does not keep: a non-empty `mcp.servers`
|
|
22007
|
+
* envelope, and a non-empty global `mcp.timeout`.
|
|
22008
|
+
*/
|
|
22009
|
+
function findOpencodeV2McpContainers(mcp) {
|
|
22010
|
+
if (!isRecord$1(mcp)) return [];
|
|
22011
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
22012
|
+
return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
|
|
22013
|
+
}
|
|
22014
|
+
/**
|
|
22015
|
+
* Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
|
|
22016
|
+
* the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
|
|
22017
|
+
*
|
|
22018
|
+
* - entries of the `mcp.servers` envelope are merged into the map, a flat entry
|
|
22019
|
+
* of the same name winning;
|
|
22020
|
+
* - V2-spelled server entries (`disabled`, an object `timeout`, snake_case
|
|
22021
|
+
* `oauth` keys) are lowered by {@link lowerOpencodeV2Server};
|
|
22022
|
+
* - the global `mcp.timeout` object is skipped. V1 moves it to
|
|
22023
|
+
* `experimental.mcp_timeout`, a default request timeout that has no
|
|
22024
|
+
* canonical counterpart, so it is not read as a server either.
|
|
22025
|
+
*
|
|
22026
|
+
* Unlike V1, an envelope entry that is not a valid server is not dropped: it
|
|
22027
|
+
* is passed on, so the schema rejects it loudly rather than it vanishing
|
|
22028
|
+
* without a word. Each value that is dropped — a non-empty global timeout, a
|
|
22029
|
+
* per-server timeout object or `oauth` key with no V1 equivalent, or an
|
|
22030
|
+
* envelope entry shadowed by a different flat entry — is reported through
|
|
22031
|
+
* `onDropped`.
|
|
22032
|
+
*
|
|
22033
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
22034
|
+
* @see https://opencode.ai/v2/docs/mcp-servers/
|
|
22035
|
+
*/
|
|
22036
|
+
function lowerOpencodeV2Mcp(mcp, onDropped) {
|
|
22037
|
+
if (!isRecord$1(mcp)) return mcp;
|
|
22038
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
22039
|
+
const globalTimeout = isOpencodeV2GlobalTimeout(mcp.timeout);
|
|
22040
|
+
if (findOpencodeV2McpContainers(mcp).includes("mcp.timeout")) onDropped?.("mcp.timeout", "unsupported");
|
|
22041
|
+
const servers = {};
|
|
22042
|
+
for (const [name, value] of Object.entries(mcp)) {
|
|
22043
|
+
if (name === "servers" && envelope !== null || name === "timeout" && globalTimeout) continue;
|
|
22044
|
+
setOwnEntry(servers, name, isRecord$1(value) && isOpencodeV2NativeServer(value) ? lowerOpencodeV2Server(value, `mcp.${name}`, onDropped) : value);
|
|
22045
|
+
}
|
|
22046
|
+
for (const [name, value] of Object.entries(envelope ?? {})) {
|
|
22047
|
+
const path = `mcp.servers.${name}`;
|
|
22048
|
+
if (Object.hasOwn(servers, name)) {
|
|
22049
|
+
if (!isDeepStrictEqual(servers[name], value)) onDropped?.(path, "conflict");
|
|
22050
|
+
continue;
|
|
22051
|
+
}
|
|
22052
|
+
setOwnEntry(servers, name, lowerOpencodeV2Server(value, path, onDropped));
|
|
22053
|
+
}
|
|
22054
|
+
return servers;
|
|
22055
|
+
}
|
|
21814
22056
|
/** Matches a string whose entire value is an OpenCode `{file:...}` reference. */
|
|
21815
22057
|
const OPENCODE_FILE_TEMPLATE_PATTERN = /^\s*\{file:(.+?)\}\s*$/;
|
|
21816
22058
|
/**
|
|
@@ -22242,6 +22484,7 @@ const QODER_DIR = ".qoder";
|
|
|
22242
22484
|
const QODER_RULE_FILE_NAME = "AGENTS.md";
|
|
22243
22485
|
const QODER_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
|
|
22244
22486
|
const QODER_RULES_DIR_PATH = join(QODER_DIR, "rules");
|
|
22487
|
+
const QODER_OUTPUT_STYLES_DIR_PATH = join(QODER_DIR, "output-styles");
|
|
22245
22488
|
const QODER_SETTINGS_FILE_NAME = "settings.json";
|
|
22246
22489
|
const QODER_PROJECT_MCP_FILE_NAME = ".mcp.json";
|
|
22247
22490
|
const QODER_COMMANDS_DIR_PATH = join(QODER_DIR, "commands");
|
|
@@ -29616,6 +29859,11 @@ const CORTEXCODE_SPEC = {
|
|
|
29616
29859
|
canonical: "enabled",
|
|
29617
29860
|
tool: "enabled"
|
|
29618
29861
|
}],
|
|
29862
|
+
stringPassthroughFields: [{
|
|
29863
|
+
canonical: "statusMessage",
|
|
29864
|
+
tool: "statusMessage",
|
|
29865
|
+
commandOnly: true
|
|
29866
|
+
}],
|
|
29619
29867
|
objectPassthroughFields: [{
|
|
29620
29868
|
canonical: "source",
|
|
29621
29869
|
tool: "source",
|
|
@@ -29630,7 +29878,7 @@ const CORTEXCODE_SPEC = {
|
|
|
29630
29878
|
* (project scope) and of the dedicated `~/.snowflake/cortex/hooks.json` (user
|
|
29631
29879
|
* scope), both in the Claude-Code shape: `{ "<Event>": [{ "matcher"?:
|
|
29632
29880
|
* "<regex>", "hooks": [{ "type": "command" | "prompt", ..., "timeout"?:
|
|
29633
|
-
* <seconds>, "enabled"?: <boolean>, "source"?: { "source", "ref"? } }] }] }`.
|
|
29881
|
+
* <seconds>, "enabled"?: <boolean>, "statusMessage"?: <string> (command only), "source"?: { "source", "ref"? } }] }] }`.
|
|
29634
29882
|
* The project file also holds settings rulesync does not own, so generation
|
|
29635
29883
|
* merges the `hooks` key into
|
|
29636
29884
|
* either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it, and
|
|
@@ -30569,7 +30817,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
|
|
|
30569
30817
|
"allowManagedHooksOnly",
|
|
30570
30818
|
"disabledSkills",
|
|
30571
30819
|
"modelPolicy",
|
|
30572
|
-
"missionPolicy"
|
|
30820
|
+
"missionPolicy",
|
|
30821
|
+
"permissionRules"
|
|
30573
30822
|
];
|
|
30574
30823
|
//#endregion
|
|
30575
30824
|
//#region src/utils/factorydroid-settings.ts
|
|
@@ -30784,7 +31033,7 @@ const GITLABDUO_CONVERTER_CONFIG = {
|
|
|
30784
31033
|
*
|
|
30785
31034
|
* Hooks live in a dedicated file, `<project>/.gitlab/duo/hooks.json` (project,
|
|
30786
31035
|
* opt-in via `--enable-project-hooks` / `GITLAB_ENABLE_PROJECT_HOOKS=true`) or
|
|
30787
|
-
* `~/.gitlab/duo/hooks.json` (user), shaped as `{ "hooks": { "SessionStart": [...] } }`.
|
|
31036
|
+
* `~/.gitlab/duo/hooks.json` (user), shaped as `{ "hooks": { "SessionStart": [...], "PreToolUse": [...] } }`.
|
|
30788
31037
|
* rulesync owns the whole file, so it is regenerated and deleted as a unit.
|
|
30789
31038
|
*/
|
|
30790
31039
|
var GitlabduoHooks = class GitlabduoHooks extends ToolHooks {
|
|
@@ -36438,6 +36687,7 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
|
|
|
36438
36687
|
const CODEBUFF_IGNORE_FILE_NAME = ".codebuffignore";
|
|
36439
36688
|
const CODEBUFF_AGENTS_DIR = ".agents";
|
|
36440
36689
|
const CODEBUFF_MCP_FILE_NAME = "mcp.json";
|
|
36690
|
+
const CODEBUFF_GLOBAL_RULE_FILE_NAME = ".AGENTS.md";
|
|
36441
36691
|
//#endregion
|
|
36442
36692
|
//#region src/features/ignore/codebuff-ignore.ts
|
|
36443
36693
|
/**
|
|
@@ -43339,11 +43589,45 @@ const CANONICAL_TO_GITLABDUO_TRANSPORT = {
|
|
|
43339
43589
|
sse: "sse"
|
|
43340
43590
|
};
|
|
43341
43591
|
/**
|
|
43592
|
+
* Map the canonical `timeout` (milliseconds) onto GitLab Duo's `timeoutMs`, a
|
|
43593
|
+
* per-server tool-call inactivity timeout (gitlab-lsp v9.25.0+) that must be a
|
|
43594
|
+
* positive integer. An explicit `timeoutMs` on the same server wins; a
|
|
43595
|
+
* `timeout` Duo would reject is dropped with a warning.
|
|
43596
|
+
*
|
|
43597
|
+
* @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/commit/e8c4a76a66e8f37d4c97fc03ffe983bfadf43f76
|
|
43598
|
+
*/
|
|
43599
|
+
function toGitlabduoTimeout({ serverName, server, logger }) {
|
|
43600
|
+
const { timeout, ...rest } = server;
|
|
43601
|
+
if (timeout === void 0 || rest.timeoutMs !== void 0) return rest;
|
|
43602
|
+
if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout <= 0) {
|
|
43603
|
+
logger?.warn(`GitLab Duo CLI MCP server "${serverName}": dropping timeout ${JSON.stringify(timeout)}, as timeoutMs must be a positive integer of milliseconds.`);
|
|
43604
|
+
return rest;
|
|
43605
|
+
}
|
|
43606
|
+
return {
|
|
43607
|
+
...rest,
|
|
43608
|
+
timeoutMs: timeout
|
|
43609
|
+
};
|
|
43610
|
+
}
|
|
43611
|
+
/**
|
|
43612
|
+
* Read GitLab Duo's `timeoutMs` back into the canonical `timeout`
|
|
43613
|
+
* (milliseconds). A server that already carries a `timeout` is left as is, so
|
|
43614
|
+
* both keys round-trip and `timeoutMs` keeps winning on the next generate.
|
|
43615
|
+
*/
|
|
43616
|
+
function fromGitlabduoTimeout(server) {
|
|
43617
|
+
if (typeof server !== "object" || server === null || Array.isArray(server)) return server;
|
|
43618
|
+
const { timeoutMs, ...rest } = server;
|
|
43619
|
+
if (timeoutMs === void 0 || rest.timeout !== void 0) return server;
|
|
43620
|
+
return {
|
|
43621
|
+
...rest,
|
|
43622
|
+
timeout: timeoutMs
|
|
43623
|
+
};
|
|
43624
|
+
}
|
|
43625
|
+
/**
|
|
43342
43626
|
* Convert one canonical server into GitLab Duo's shape. Every documented
|
|
43343
43627
|
* example spells the transport out as `type` (`stdio`, `http` or `sse`), so it
|
|
43344
43628
|
* is always written, inferred from `command` / `url` when the source leaves it
|
|
43345
|
-
* implicit.
|
|
43346
|
-
* through as authored.
|
|
43629
|
+
* implicit. The canonical `timeout` becomes `timeoutMs`; every other field —
|
|
43630
|
+
* `args`, `env`, `approvedTools`, ... — is carried through as authored.
|
|
43347
43631
|
*/
|
|
43348
43632
|
function toGitlabduoServer({ serverName, serverConfig, logger }) {
|
|
43349
43633
|
if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
|
|
@@ -43352,7 +43636,7 @@ function toGitlabduoServer({ serverName, serverConfig, logger }) {
|
|
|
43352
43636
|
reason: "no command or url",
|
|
43353
43637
|
logger
|
|
43354
43638
|
});
|
|
43355
|
-
const { type, transport, httpUrl: _httpUrl, url: _url, ...
|
|
43639
|
+
const { type, transport, httpUrl: _httpUrl, url: _url, ...others } = serverConfig;
|
|
43356
43640
|
const declared = type ?? transport;
|
|
43357
43641
|
const mapped = declared ? CANONICAL_TO_GITLABDUO_TRANSPORT[declared] : isRemoteMcpServer(serverConfig) ? "http" : "stdio";
|
|
43358
43642
|
if (!mapped) return warnAndSkipMcpServer({
|
|
@@ -43361,8 +43645,13 @@ function toGitlabduoServer({ serverName, serverConfig, logger }) {
|
|
|
43361
43645
|
reason: `the unsupported transport "${declared}" (GitLab Duo CLI accepts stdio, http and sse)`,
|
|
43362
43646
|
logger
|
|
43363
43647
|
});
|
|
43648
|
+
const rest = toGitlabduoTimeout({
|
|
43649
|
+
serverName,
|
|
43650
|
+
server: others,
|
|
43651
|
+
logger
|
|
43652
|
+
});
|
|
43364
43653
|
if (mapped === "stdio") {
|
|
43365
|
-
const { command, args } =
|
|
43654
|
+
const { command, args } = serverConfig;
|
|
43366
43655
|
if (Array.isArray(command)) {
|
|
43367
43656
|
const [executable, ...leadingArgs] = command;
|
|
43368
43657
|
return {
|
|
@@ -43451,7 +43740,13 @@ var GitlabduoMcp = class GitlabduoMcp extends ToolMcp {
|
|
|
43451
43740
|
});
|
|
43452
43741
|
}
|
|
43453
43742
|
toRulesyncMcp() {
|
|
43454
|
-
|
|
43743
|
+
const { mcpServers, ...others } = this.json;
|
|
43744
|
+
if (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers)) return this.toRulesyncMcpDefault();
|
|
43745
|
+
const servers = Object.fromEntries(Object.entries(mcpServers).map(([name, server]) => [name, fromGitlabduoTimeout(server)]));
|
|
43746
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
|
|
43747
|
+
...others,
|
|
43748
|
+
mcpServers: servers
|
|
43749
|
+
}) });
|
|
43455
43750
|
}
|
|
43456
43751
|
validate() {
|
|
43457
43752
|
return {
|
|
@@ -45559,18 +45854,21 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
45559
45854
|
};
|
|
45560
45855
|
//#endregion
|
|
45561
45856
|
//#region src/features/mcp/opencode-mcp.ts
|
|
45857
|
+
const OpencodeMcpV2DisabledSchema = z.optional(z.boolean());
|
|
45562
45858
|
const OpencodeMcpLocalServerSchema = z.looseObject({
|
|
45563
45859
|
type: z.literal("local"),
|
|
45564
45860
|
command: z.array(z.string()),
|
|
45565
45861
|
environment: z.optional(z.record(z.string(), z.string())),
|
|
45566
45862
|
enabled: z._default(z.boolean(), true),
|
|
45863
|
+
disabled: OpencodeMcpV2DisabledSchema,
|
|
45567
45864
|
cwd: z.optional(z.string())
|
|
45568
45865
|
});
|
|
45569
45866
|
const OpencodeMcpRemoteServerSchema = z.looseObject({
|
|
45570
45867
|
type: z.literal("remote"),
|
|
45571
45868
|
url: z.string(),
|
|
45572
45869
|
headers: z.optional(z.record(z.string(), z.string())),
|
|
45573
|
-
enabled: z._default(z.boolean(), true)
|
|
45870
|
+
enabled: z._default(z.boolean(), true),
|
|
45871
|
+
disabled: OpencodeMcpV2DisabledSchema
|
|
45574
45872
|
});
|
|
45575
45873
|
const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
45576
45874
|
"timeout",
|
|
@@ -45582,7 +45880,7 @@ const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
|
45582
45880
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
45583
45881
|
...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
|
|
45584
45882
|
...OPENCODE_PASSTHROUGH_SERVER_FIELDS
|
|
45585
|
-
].filter((key) => key !== "enabled");
|
|
45883
|
+
].filter((key) => key !== "enabled" && key !== "disabled");
|
|
45586
45884
|
/**
|
|
45587
45885
|
* A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
|
|
45588
45886
|
* disabling a server another config layer defines. It is the third member of
|
|
@@ -45710,7 +46008,7 @@ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
|
|
|
45710
46008
|
function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
45711
46009
|
const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
|
|
45712
46010
|
if (dropped.length === 0) return;
|
|
45713
|
-
logger?.warn(`OpenCode MCP:
|
|
46011
|
+
logger?.warn(`OpenCode MCP: ${quoteValueForWarning(serverName)} declares no transport, so it is written as a toggle entry and ${dropped.toSorted().map(quoteValueForWarning).join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
|
|
45714
46012
|
}
|
|
45715
46013
|
/**
|
|
45716
46014
|
* Convert standard MCP format to OpenCode native format
|
|
@@ -45790,9 +46088,35 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45790
46088
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
45791
46089
|
static layout = OPENCODE_LAYOUT;
|
|
45792
46090
|
json;
|
|
45793
|
-
|
|
46091
|
+
/**
|
|
46092
|
+
* With a `logger`, the V2 values that lowering the content drops are warned
|
|
46093
|
+
* about (see {@link OpencodeMcp.lowerConfig}); `fromFile` passes one so the
|
|
46094
|
+
* warnings come from the lowering that builds the parsed view rather than
|
|
46095
|
+
* from an extra pass of their own. (`validate()`, which the parent
|
|
46096
|
+
* constructor calls before this view exists, lowers separately and silently.)
|
|
46097
|
+
*/
|
|
46098
|
+
constructor({ logger, ...params }) {
|
|
45794
46099
|
super(params);
|
|
45795
|
-
this.json = OpencodeConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
46100
|
+
this.json = OpencodeConfigSchema.parse(this.constructor.lowerConfig(parse$1(this.fileContent || "{}"), logger));
|
|
46101
|
+
}
|
|
46102
|
+
/**
|
|
46103
|
+
* Lowers the OpenCode V2 `mcp` spellings of a parsed config for a layout
|
|
46104
|
+
* that reads them (see {@link lowerOpencodeV2Mcp}), so every reader — import,
|
|
46105
|
+
* the toggle lookup on generate, and the rules feature's `instructions`
|
|
46106
|
+
* write to the same file — accepts a V2-shaped `mcp`. Only the parsed view
|
|
46107
|
+
* is lowered; content written back keeps whatever `mcp` a feature that does
|
|
46108
|
+
* not own the key found there. With a `logger`, each V2 value the lowering
|
|
46109
|
+
* drops is warned about.
|
|
46110
|
+
*/
|
|
46111
|
+
static lowerConfig(json, logger) {
|
|
46112
|
+
if (!this.layout.readsV2ConfigSpellings || !isRecord$1(json) || json.mcp === void 0) return json;
|
|
46113
|
+
return {
|
|
46114
|
+
...json,
|
|
46115
|
+
mcp: lowerOpencodeV2Mcp(json.mcp, (path, reason) => {
|
|
46116
|
+
const quoted = quoteValueForWarning(path);
|
|
46117
|
+
logger?.warn(reason === "conflict" ? `OpenCode MCP: ${quoted} conflicts with a V1 value that takes precedence (the flat "mcp" entry of the same name, or "enabled"), so it is dropped on import.` : `OpenCode MCP: ${quoted} has no Rulesync equivalent, so it is dropped on import.`);
|
|
46118
|
+
})
|
|
46119
|
+
};
|
|
45796
46120
|
}
|
|
45797
46121
|
getJson() {
|
|
45798
46122
|
return this.json;
|
|
@@ -45809,7 +46133,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45809
46133
|
relativeFilePath: this.layout.jsonFileName
|
|
45810
46134
|
};
|
|
45811
46135
|
}
|
|
45812
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
46136
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
45813
46137
|
const basePaths = this.getSettablePaths({ global });
|
|
45814
46138
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
45815
46139
|
let fileContent = null;
|
|
@@ -45831,7 +46155,8 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45831
46155
|
relativeDirPath: basePaths.relativeDirPath,
|
|
45832
46156
|
relativeFilePath,
|
|
45833
46157
|
fileContent: JSON.stringify(newJson, null, 2),
|
|
45834
|
-
validate
|
|
46158
|
+
validate,
|
|
46159
|
+
logger
|
|
45835
46160
|
});
|
|
45836
46161
|
}
|
|
45837
46162
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
@@ -45850,7 +46175,12 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45850
46175
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
45851
46176
|
replacement: "{env:$1}"
|
|
45852
46177
|
});
|
|
45853
|
-
const
|
|
46178
|
+
const existingJson = parse$1(fileContent || "{}");
|
|
46179
|
+
const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
|
|
46180
|
+
if (this.layout.readsV2ConfigSpellings && isRecord$1(existingJson)) {
|
|
46181
|
+
const containers = findOpencodeV2McpContainers(existingJson.mcp);
|
|
46182
|
+
if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
|
|
46183
|
+
}
|
|
45854
46184
|
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
45855
46185
|
return new this({
|
|
45856
46186
|
outputRoot,
|
|
@@ -45950,7 +46280,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45950
46280
|
}
|
|
45951
46281
|
validate() {
|
|
45952
46282
|
const json = parseJsonc(this.fileContent || "{}");
|
|
45953
|
-
const result = OpencodeConfigSchema.safeParse(json);
|
|
46283
|
+
const result = OpencodeConfigSchema.safeParse(this.constructor.lowerConfig(json));
|
|
45954
46284
|
if (!result.success) return {
|
|
45955
46285
|
success: false,
|
|
45956
46286
|
error: result.error
|
|
@@ -46523,6 +46853,15 @@ const CONSUMED_CANONICAL_KEYS = /* @__PURE__ */ new Set([
|
|
|
46523
46853
|
]);
|
|
46524
46854
|
const MILLISECONDS_PER_SECOND = 1e3;
|
|
46525
46855
|
/**
|
|
46856
|
+
* The only keys a project override entry for a user-level server carries:
|
|
46857
|
+
* Pi reads a `.pi/mcp.json` entry without `command`, `url` or `type` as an
|
|
46858
|
+
* override of `enabled`, `exposure` and `toolExposure` of the user-level server
|
|
46859
|
+
* with the same name, and keeps the rest of that server.
|
|
46860
|
+
*
|
|
46861
|
+
* @see https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/docs/mcp.md
|
|
46862
|
+
*/
|
|
46863
|
+
const PI_OVERRIDE_PASSTHROUGH_KEYS = ["exposure", "toolExposure"];
|
|
46864
|
+
/**
|
|
46526
46865
|
* Pi rejects a server whose name has any other character, and treats names
|
|
46527
46866
|
* that differ only in `-` and `_` as one server (its tools share the
|
|
46528
46867
|
* `mcp__<name>` namespace with `-` replaced by `_`), rejecting the second.
|
|
@@ -46558,17 +46897,12 @@ function hasUnexpandedEnvVarRef(serverConfig) {
|
|
|
46558
46897
|
/**
|
|
46559
46898
|
* The transport fields Pi reads for one server: `type` `stdio` with a string
|
|
46560
46899
|
* `command` plus `args`, or `type` `http` with a `url`. A server Pi cannot
|
|
46561
|
-
* start or reach —
|
|
46562
|
-
*
|
|
46563
|
-
*
|
|
46900
|
+
* start or reach — a remote transport without a URL, an SSE or WebSocket
|
|
46901
|
+
* server, or a stdio entry without a command — is rejected by Pi, so it is
|
|
46902
|
+
* skipped with a warning (`null`). A server with no transport at all is a
|
|
46903
|
+
* project override instead (see `convertOverrideToPi`).
|
|
46564
46904
|
*/
|
|
46565
46905
|
function convertTransportToPi({ serverName, serverConfig, logger }) {
|
|
46566
|
-
if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
|
|
46567
|
-
toolName: "Pi",
|
|
46568
|
-
serverName,
|
|
46569
|
-
reason: "no transport",
|
|
46570
|
-
logger
|
|
46571
|
-
});
|
|
46572
46906
|
if (isRemoteMcpServer(serverConfig)) {
|
|
46573
46907
|
const url = resolveRemoteMcpUrl(serverConfig);
|
|
46574
46908
|
const stated = serverConfig.type ?? serverConfig.transport;
|
|
@@ -46609,56 +46943,117 @@ function convertTransportToPi({ serverName, serverConfig, logger }) {
|
|
|
46609
46943
|
};
|
|
46610
46944
|
}
|
|
46611
46945
|
/**
|
|
46612
|
-
*
|
|
46613
|
-
*
|
|
46614
|
-
*
|
|
46615
|
-
*
|
|
46616
|
-
*
|
|
46946
|
+
* A transport-less server in the project file becomes a Pi override entry for
|
|
46947
|
+
* the user-level server of the same name. `disabled` maps to `enabled` both
|
|
46948
|
+
* ways (an explicit `disabled: false` is "Enable in this project"), and any key
|
|
46949
|
+
* Pi ignores on an override is dropped with a warning. The global file has no
|
|
46950
|
+
* layer below it to override, so the entry is skipped there.
|
|
46951
|
+
*/
|
|
46952
|
+
function convertOverrideToPi({ serverName, serverConfig, global, logger }) {
|
|
46953
|
+
if (global) return warnAndSkipMcpServer({
|
|
46954
|
+
toolName: "Pi",
|
|
46955
|
+
serverName,
|
|
46956
|
+
reason: "no transport",
|
|
46957
|
+
logger
|
|
46958
|
+
});
|
|
46959
|
+
const record = serverConfig;
|
|
46960
|
+
const override = {};
|
|
46961
|
+
if (typeof serverConfig.disabled === "boolean") override.enabled = !serverConfig.disabled;
|
|
46962
|
+
for (const key of PI_OVERRIDE_PASSTHROUGH_KEYS) if (record[key] !== void 0) override[key] = record[key];
|
|
46963
|
+
if (Object.keys(override).length === 0) return warnAndSkipMcpServer({
|
|
46964
|
+
toolName: "Pi",
|
|
46965
|
+
serverName,
|
|
46966
|
+
reason: "no transport and nothing a Pi project override sets (disabled, exposure, toolExposure)",
|
|
46967
|
+
logger
|
|
46968
|
+
});
|
|
46969
|
+
const droppedKeys = Object.keys(record).filter((key) => key !== "disabled" && !PI_OVERRIDE_PASSTHROUGH_KEYS.includes(key) && !PROTOTYPE_POLLUTION_KEYS.has(key));
|
|
46970
|
+
if (droppedKeys.length > 0) logger?.warn(`Pi MCP: dropping ${droppedKeys.map((key) => quoteValueForWarning(key)).join(", ")} of the project override ${quoteValueForWarning(serverName)}: Pi reads only enabled, exposure and toolExposure from an entry without a transport.`);
|
|
46971
|
+
return override;
|
|
46972
|
+
}
|
|
46973
|
+
/**
|
|
46974
|
+
* Whether Pi rejects the server name, either outright or because an earlier
|
|
46975
|
+
* server already took its `mcp__<name>` namespace; warns when it does.
|
|
46976
|
+
*/
|
|
46977
|
+
function isRejectedServerName({ serverName, namespaces, logger }) {
|
|
46978
|
+
if (!PI_SERVER_NAME_PATTERN.test(serverName)) {
|
|
46979
|
+
warnAndSkipMcpServer({
|
|
46980
|
+
toolName: "Pi",
|
|
46981
|
+
serverName,
|
|
46982
|
+
reason: "a name Pi rejects (use letters, digits, \"_\" and \"-\")",
|
|
46983
|
+
logger
|
|
46984
|
+
});
|
|
46985
|
+
return true;
|
|
46986
|
+
}
|
|
46987
|
+
const clash = namespaces.get(toPiNamespace(serverName));
|
|
46988
|
+
if (clash !== void 0) {
|
|
46989
|
+
warnAndSkipMcpServer({
|
|
46990
|
+
toolName: "Pi",
|
|
46991
|
+
serverName,
|
|
46992
|
+
reason: `a name Pi treats as the same server as ${quoteValueForWarning(clash)} ("-" and "_" are equivalent)`,
|
|
46993
|
+
logger
|
|
46994
|
+
});
|
|
46995
|
+
return true;
|
|
46996
|
+
}
|
|
46997
|
+
return false;
|
|
46998
|
+
}
|
|
46999
|
+
/**
|
|
47000
|
+
* Convert one canonical server to its Pi entry, or `null` when it is skipped.
|
|
47001
|
+
* Besides the transport fields, Pi switches a server off with `enabled: false`
|
|
47002
|
+
* and reads `timeout` in seconds (canonical: milliseconds). `auth` is dropped
|
|
47003
|
+
* with a warning from the project file, since Pi accepts it only in the global
|
|
47004
|
+
* one (it drops the whole server otherwise). A server without a transport is a
|
|
47005
|
+
* project override entry instead.
|
|
47006
|
+
*/
|
|
47007
|
+
function convertServerToPi({ serverName, serverConfig, global, logger }) {
|
|
47008
|
+
if (declaresNoTransport(serverConfig)) return convertOverrideToPi({
|
|
47009
|
+
serverName,
|
|
47010
|
+
serverConfig,
|
|
47011
|
+
global,
|
|
47012
|
+
logger
|
|
47013
|
+
});
|
|
47014
|
+
const converted = convertTransportToPi({
|
|
47015
|
+
serverName,
|
|
47016
|
+
serverConfig,
|
|
47017
|
+
logger
|
|
47018
|
+
});
|
|
47019
|
+
if (!converted) return null;
|
|
47020
|
+
for (const [key, value] of Object.entries(serverConfig)) {
|
|
47021
|
+
if (CONSUMED_CANONICAL_KEYS.has(key) || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
47022
|
+
converted[key] = value;
|
|
47023
|
+
}
|
|
47024
|
+
if (serverConfig.timeout !== void 0) {
|
|
47025
|
+
if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout > 0) converted.timeout = serverConfig.timeout / MILLISECONDS_PER_SECOND;
|
|
47026
|
+
else logger?.warn(`Pi MCP: dropping the "timeout" of ${quoteValueForWarning(serverName)}: expected a positive number of milliseconds.`);
|
|
47027
|
+
}
|
|
47028
|
+
if (serverConfig.disabled === true) converted.enabled = false;
|
|
47029
|
+
if (!global && "auth" in converted) {
|
|
47030
|
+
delete converted.auth;
|
|
47031
|
+
logger?.warn(`Pi MCP: dropping the "auth" of ${quoteValueForWarning(serverName)}: Pi accepts it only in the global ~/.pi/agent/mcp.json.`);
|
|
47032
|
+
}
|
|
47033
|
+
if (hasUnexpandedEnvVarRef(converted)) logger?.warn(`Pi MCP: ${quoteValueForWarning(serverName)} carries an environment variable reference Pi does not expand. Pi expands only \${VAR} and $VAR in env and headers values, and only a leading ~/ in command, args and cwd.`);
|
|
47034
|
+
return converted;
|
|
47035
|
+
}
|
|
47036
|
+
/**
|
|
47037
|
+
* Convert the canonical server map to Pi's `mcpServers` shape. A server name
|
|
47038
|
+
* Pi rejects is skipped with a warning.
|
|
46617
47039
|
*/
|
|
46618
47040
|
function convertToPiFormat({ mcpServers, global, logger }) {
|
|
46619
47041
|
const result = {};
|
|
46620
47042
|
const namespaces = /* @__PURE__ */ new Map();
|
|
46621
47043
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
46622
47044
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
46623
|
-
if (
|
|
46624
|
-
|
|
46625
|
-
|
|
46626
|
-
|
|
46627
|
-
|
|
46628
|
-
|
|
46629
|
-
});
|
|
46630
|
-
continue;
|
|
46631
|
-
}
|
|
46632
|
-
const clash = namespaces.get(toPiNamespace(serverName));
|
|
46633
|
-
if (clash !== void 0) {
|
|
46634
|
-
warnAndSkipMcpServer({
|
|
46635
|
-
toolName: "Pi",
|
|
46636
|
-
serverName,
|
|
46637
|
-
reason: `a name Pi treats as the same server as ${quoteValueForWarning(clash)} ("-" and "_" are equivalent)`,
|
|
46638
|
-
logger
|
|
46639
|
-
});
|
|
46640
|
-
continue;
|
|
46641
|
-
}
|
|
46642
|
-
const converted = convertTransportToPi({
|
|
47045
|
+
if (isRejectedServerName({
|
|
47046
|
+
serverName,
|
|
47047
|
+
namespaces,
|
|
47048
|
+
logger
|
|
47049
|
+
})) continue;
|
|
47050
|
+
const converted = convertServerToPi({
|
|
46643
47051
|
serverName,
|
|
46644
47052
|
serverConfig,
|
|
47053
|
+
global,
|
|
46645
47054
|
logger
|
|
46646
47055
|
});
|
|
46647
47056
|
if (!converted) continue;
|
|
46648
|
-
for (const [key, value] of Object.entries(serverConfig)) {
|
|
46649
|
-
if (CONSUMED_CANONICAL_KEYS.has(key) || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
46650
|
-
converted[key] = value;
|
|
46651
|
-
}
|
|
46652
|
-
if (serverConfig.timeout !== void 0) {
|
|
46653
|
-
if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout > 0) converted.timeout = serverConfig.timeout / MILLISECONDS_PER_SECOND;
|
|
46654
|
-
else logger?.warn(`Pi MCP: dropping the "timeout" of ${quoteValueForWarning(serverName)}: expected a positive number of milliseconds.`);
|
|
46655
|
-
}
|
|
46656
|
-
if (serverConfig.disabled === true) converted.enabled = false;
|
|
46657
|
-
if (!global && "auth" in converted) {
|
|
46658
|
-
delete converted.auth;
|
|
46659
|
-
logger?.warn(`Pi MCP: dropping the "auth" of ${quoteValueForWarning(serverName)}: Pi accepts it only in the global ~/.pi/agent/mcp.json.`);
|
|
46660
|
-
}
|
|
46661
|
-
if (hasUnexpandedEnvVarRef(converted)) logger?.warn(`Pi MCP: ${quoteValueForWarning(serverName)} carries an environment variable reference Pi does not expand. Pi expands only \${VAR} and $VAR in env and headers values, and only a leading ~/ in command, args and cwd.`);
|
|
46662
47057
|
namespaces.set(toPiNamespace(serverName), serverName);
|
|
46663
47058
|
result[serverName] = converted;
|
|
46664
47059
|
}
|
|
@@ -46667,16 +47062,19 @@ function convertToPiFormat({ mcpServers, global, logger }) {
|
|
|
46667
47062
|
/**
|
|
46668
47063
|
* Convert Pi's `mcpServers` back to the canonical shape: `enabled: false`
|
|
46669
47064
|
* becomes `disabled: true` and `timeout` (seconds) becomes milliseconds; every
|
|
46670
|
-
* other key is already canonical.
|
|
47065
|
+
* other key is already canonical. On a transport-less override entry an
|
|
47066
|
+
* explicit `enabled: true` is kept as `disabled: false`, since it switches on a
|
|
47067
|
+
* user-level server the user turned off.
|
|
46671
47068
|
*/
|
|
46672
47069
|
function convertFromPiFormat(mcpServers) {
|
|
46673
47070
|
const result = {};
|
|
46674
47071
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
46675
47072
|
if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
|
|
46676
47073
|
const { enabled, timeout, ...rest } = serverConfig;
|
|
46677
|
-
const
|
|
47074
|
+
const keepsEnabled = enabled === true && declaresNoTransport(rest);
|
|
47075
|
+
const converted = enabled === false || keepsEnabled ? {
|
|
46678
47076
|
...rest,
|
|
46679
|
-
disabled:
|
|
47077
|
+
disabled: !enabled
|
|
46680
47078
|
} : rest;
|
|
46681
47079
|
if (typeof timeout === "number") converted.timeout = timeout * MILLISECONDS_PER_SECOND;
|
|
46682
47080
|
else if (timeout !== void 0) converted.timeout = timeout;
|
|
@@ -46752,8 +47150,11 @@ var PiMcp = class PiMcp extends ToolMcp {
|
|
|
46752
47150
|
});
|
|
46753
47151
|
}
|
|
46754
47152
|
toRulesyncMcp() {
|
|
46755
|
-
const
|
|
46756
|
-
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
|
|
47153
|
+
const { shared, toolOnly } = splitMcpServersByTransport(convertFromPiFormat(isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {}));
|
|
47154
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
|
|
47155
|
+
mcpServers: shared,
|
|
47156
|
+
...Object.keys(toolOnly).length > 0 && { pi: { mcpServers: toolOnly } }
|
|
47157
|
+
}, null, 2) });
|
|
46757
47158
|
}
|
|
46758
47159
|
validate() {
|
|
46759
47160
|
return {
|
|
@@ -64546,9 +64947,11 @@ const DEFAULT_TOOLS_KEY = "defaultTools";
|
|
|
64546
64947
|
* the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
|
|
64547
64948
|
* permissions override.
|
|
64548
64949
|
*
|
|
64549
|
-
* Scope
|
|
64550
|
-
* a project `defaultTools` array *replaces* the global
|
|
64551
|
-
*
|
|
64950
|
+
* Scope layering is Pi's own rather than the union-merge most targets use:
|
|
64951
|
+
* a project `defaultTools` array with a plain name *replaces* the global
|
|
64952
|
+
* array, while one made only of `+name` / `-name` entries (Pi v0.99.0+) changes
|
|
64953
|
+
* the global selection instead. Entries are written verbatim, so the two
|
|
64954
|
+
* scopes are written independently and never combined by rulesync.
|
|
64552
64955
|
* Project scope writes `.pi/settings.json`; global scope writes
|
|
64553
64956
|
* `~/.pi/agent/settings.json`.
|
|
64554
64957
|
*
|
|
@@ -65208,6 +65611,13 @@ function convertPoolToRulesync({ tools, paths }) {
|
|
|
65208
65611
|
//#region src/features/permissions/qoder-permissions.ts
|
|
65209
65612
|
/** Top-level key of `.qoder/settings.json` that rulesync owns. */
|
|
65210
65613
|
const QODER_PERMISSIONS_KEY = "permissions";
|
|
65614
|
+
/**
|
|
65615
|
+
* Top-level key the `qoder` override authors: the active output style. Qoder
|
|
65616
|
+
* also reads `general.outputStyle`, but the top-level key wins, so that is the
|
|
65617
|
+
* one written and imported.
|
|
65618
|
+
* @see https://docs.qoder.com/cli/settings-reference
|
|
65619
|
+
*/
|
|
65620
|
+
const QODER_OUTPUT_STYLE_KEY = "outputStyle";
|
|
65211
65621
|
const CATCH_ALL_PATTERN$2 = "*";
|
|
65212
65622
|
/** Prefix of the fully qualified MCP tool names (`mcp__<server>__<tool>`). */
|
|
65213
65623
|
const MCP_TOOL_PREFIX = "mcp__";
|
|
@@ -65367,9 +65777,11 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
|
|
|
65367
65777
|
* through the shared-config gateway, the sibling keys of `permissions`
|
|
65368
65778
|
* (`additionalDirectories`, `trustDirectories`) are kept, and the file is
|
|
65369
65779
|
* never deleted. `.qoder/settings.local.json` (where `/allow` and `/deny`
|
|
65370
|
-
* persist) is left to the user.
|
|
65780
|
+
* persist) is left to the user. The `qoder` override's `outputStyle` is
|
|
65781
|
+
* written to (and imported from) the top-level `outputStyle` key.
|
|
65371
65782
|
*
|
|
65372
65783
|
* @see https://docs.qoder.com/en/cli/permissions
|
|
65784
|
+
* @see https://docs.qoder.com/cli/output-styles
|
|
65373
65785
|
*/
|
|
65374
65786
|
var QoderPermissions = class QoderPermissions extends ToolPermissions {
|
|
65375
65787
|
constructor(params) {
|
|
@@ -65408,11 +65820,14 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
|
|
|
65408
65820
|
relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
|
|
65409
65821
|
});
|
|
65410
65822
|
const existingPermissions = isRecord$1(existing[QODER_PERMISSIONS_KEY]) ? existing[QODER_PERMISSIONS_KEY] : {};
|
|
65823
|
+
const config = rulesyncPermissions.getJson();
|
|
65411
65824
|
const lists = buildQoderRuleLists({
|
|
65412
|
-
config
|
|
65825
|
+
config,
|
|
65413
65826
|
existingPermissions,
|
|
65414
65827
|
logger
|
|
65415
65828
|
});
|
|
65829
|
+
const outputStyle = config.qoder?.outputStyle?.trim();
|
|
65830
|
+
if (outputStyle !== void 0) logger?.warn(`Qoder permissions: qoder.outputStyle = ${JSON.stringify(outputStyle)} was written to ${join(paths.relativeDirPath, paths.relativeFilePath)}${global ? " in the home directory" : ""}, so Qoder appends that style's instructions to the system prompt of every session it applies to.`);
|
|
65416
65831
|
const permissions = { ...existingPermissions };
|
|
65417
65832
|
for (const action of ACTIONS) if (lists[action].length > 0) permissions[action] = lists[action];
|
|
65418
65833
|
else delete permissions[action];
|
|
@@ -65424,7 +65839,10 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
|
|
|
65424
65839
|
fileKey: sharedConfigFileKey(paths),
|
|
65425
65840
|
feature: "permissions",
|
|
65426
65841
|
existingContent,
|
|
65427
|
-
patch: {
|
|
65842
|
+
patch: {
|
|
65843
|
+
[QODER_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
|
|
65844
|
+
...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle }
|
|
65845
|
+
},
|
|
65428
65846
|
filePath
|
|
65429
65847
|
}),
|
|
65430
65848
|
validate: true,
|
|
@@ -65445,7 +65863,12 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
|
|
|
65445
65863
|
const existing = rules[parsed.pattern];
|
|
65446
65864
|
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[parsed.pattern] = action;
|
|
65447
65865
|
}
|
|
65448
|
-
|
|
65866
|
+
const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
|
|
65867
|
+
const hasOutputStyle = typeof outputStyle === "string" && outputStyle.trim() !== "";
|
|
65868
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
|
|
65869
|
+
permission,
|
|
65870
|
+
...hasOutputStyle && { qoder: { outputStyle } }
|
|
65871
|
+
}, null, 2) });
|
|
65449
65872
|
}
|
|
65450
65873
|
validate() {
|
|
65451
65874
|
return {
|
|
@@ -84493,8 +84916,8 @@ var DevinSubagent = class extends ToolSubagent {
|
|
|
84493
84916
|
* Derive the subagent name from this instance's relative file path.
|
|
84494
84917
|
*
|
|
84495
84918
|
* The tool-side layout is `<name>/AGENT.md`, so the name is the parent
|
|
84496
|
-
* directory of the file.
|
|
84497
|
-
*
|
|
84919
|
+
* directory of the file. Devin also reads a flat `<name>.md`, which import
|
|
84920
|
+
* picks up too; its name is the basename without extension.
|
|
84498
84921
|
*/
|
|
84499
84922
|
getSubagentName() {
|
|
84500
84923
|
const relativeFilePath = this.getRelativeFilePath();
|
|
@@ -84587,8 +85010,9 @@ var DevinSubagent = class extends ToolSubagent {
|
|
|
84587
85010
|
/**
|
|
84588
85011
|
* Subagent inside a Devin plugin bundle (`<plugin>/agents/<name>/AGENT.md`).
|
|
84589
85012
|
* Plugin subagents use the same profile format as project ones and are
|
|
84590
|
-
* exposed as `<plugin>:<name>`. Devin also reads a flat `agents/<name>.md
|
|
84591
|
-
* the directory form is written to match the
|
|
85013
|
+
* exposed as `<plugin>:<name>`. Devin also reads a flat `agents/<name>.md`,
|
|
85014
|
+
* which import picks up too; the directory form is written to match the
|
|
85015
|
+
* `devin` target.
|
|
84592
85016
|
*
|
|
84593
85017
|
* @see https://docs.devin.ai/cli/extensibility/plugins/overview
|
|
84594
85018
|
*/
|
|
@@ -88150,6 +88574,17 @@ var ZoocodeSubagent = class extends RooSubagent {
|
|
|
88150
88574
|
};
|
|
88151
88575
|
//#endregion
|
|
88152
88576
|
//#region src/features/subagents/subagents-processor.ts
|
|
88577
|
+
/**
|
|
88578
|
+
* The globs import scans for a tool: its import-only patterns when it reads
|
|
88579
|
+
* more layouts than Rulesync writes, otherwise the generation pattern.
|
|
88580
|
+
*/
|
|
88581
|
+
function importFilePatternsOf(meta) {
|
|
88582
|
+
return meta.importFilePatterns ?? meta.filePattern;
|
|
88583
|
+
}
|
|
88584
|
+
/** Whether unparseable files are left out of this import or orphan sweep. */
|
|
88585
|
+
function skipsInvalidFiles(meta, forDeletion) {
|
|
88586
|
+
return meta.skipInvalidFiles === true || !forDeletion && meta.skipInvalidImportFiles === true;
|
|
88587
|
+
}
|
|
88153
88588
|
const SubagentsProcessorToolTargetSchema = z.enum(subagentsProcessorToolTargetTuple);
|
|
88154
88589
|
/**
|
|
88155
88590
|
* Factory Map mapping tool targets to their subagent factories.
|
|
@@ -88346,7 +88781,9 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
88346
88781
|
supportsProject: true,
|
|
88347
88782
|
supportsSimulated: false,
|
|
88348
88783
|
supportsGlobal: true,
|
|
88349
|
-
filePattern: "*/AGENT.md"
|
|
88784
|
+
filePattern: "*/AGENT.md",
|
|
88785
|
+
importFilePatterns: ["*.md", "*/AGENT.md"],
|
|
88786
|
+
skipInvalidImportFiles: true
|
|
88350
88787
|
}
|
|
88351
88788
|
}],
|
|
88352
88789
|
["devin-plugin", {
|
|
@@ -88355,7 +88792,9 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
88355
88792
|
supportsProject: true,
|
|
88356
88793
|
supportsSimulated: false,
|
|
88357
88794
|
supportsGlobal: false,
|
|
88358
|
-
filePattern: "*/AGENT.md"
|
|
88795
|
+
filePattern: "*/AGENT.md",
|
|
88796
|
+
importFilePatterns: ["*.md", "*/AGENT.md"],
|
|
88797
|
+
skipInvalidImportFiles: true
|
|
88359
88798
|
}
|
|
88360
88799
|
}],
|
|
88361
88800
|
["factorydroid", {
|
|
@@ -88834,6 +89273,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
88834
89273
|
const factory = this.getFactory(this.toolTarget);
|
|
88835
89274
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
88836
89275
|
const roots = forDeletion ? [paths.relativeDirPath] : [paths.relativeDirPath, ...paths.importDirPaths ?? []];
|
|
89276
|
+
const filePatterns = forDeletion ? factory.meta.filePattern : importFilePatternsOf(factory.meta);
|
|
88837
89277
|
const toolSubagents = [];
|
|
88838
89278
|
const claimedRelativeFilePaths = /* @__PURE__ */ new Map();
|
|
88839
89279
|
const claimedImportIdentities = new ClaimedIdentities();
|
|
@@ -88850,7 +89290,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
88850
89290
|
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(baseDir)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
88851
89291
|
continue;
|
|
88852
89292
|
}
|
|
88853
|
-
const subagentFilePaths = (await findFilesByGlobs(
|
|
89293
|
+
const subagentFilePaths = (await findFilesByGlobs(filePatterns, {
|
|
88854
89294
|
cwd: baseDir,
|
|
88855
89295
|
followSymbolicLinks: !forDeletion
|
|
88856
89296
|
})).toSorted((left, right) => compareRelativePathsShallowestFirst(relative(baseDir, left), relative(baseDir, right)));
|
|
@@ -88864,7 +89304,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
88864
89304
|
})));
|
|
88865
89305
|
ownedFilePaths = subagentFilePaths.filter((_, index) => ownership[index]);
|
|
88866
89306
|
}
|
|
88867
|
-
const parsedFiles = factory.meta
|
|
89307
|
+
const parsedFiles = skipsInvalidFiles(factory.meta, forDeletion) ? (await Promise.all(ownedFilePaths.map(async (path) => {
|
|
88868
89308
|
try {
|
|
88869
89309
|
return {
|
|
88870
89310
|
path,
|
|
@@ -91740,6 +92180,85 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
|
|
|
91740
92180
|
}
|
|
91741
92181
|
};
|
|
91742
92182
|
//#endregion
|
|
92183
|
+
//#region src/features/rules/codebuff-rule.ts
|
|
92184
|
+
/**
|
|
92185
|
+
* Rule generator for Codebuff (Freebuff), global scope only.
|
|
92186
|
+
*
|
|
92187
|
+
* Codebuff reads one user-level instructions file from the home directory:
|
|
92188
|
+
* the first hidden file matching `AGENTS.md` or `CLAUDE.md` (case-insensitive,
|
|
92189
|
+
* in that priority order), so the root rule is written to `~/.AGENTS.md`. There
|
|
92190
|
+
* is no user-level rules directory, so non-root rules fold into that file.
|
|
92191
|
+
*
|
|
92192
|
+
* Project scope is not supported here: Codebuff reads the project-root
|
|
92193
|
+
* `AGENTS.md`, which the `agentsmd` target already writes. The RulesProcessor
|
|
92194
|
+
* keeps this target out of project mode (`supportsProject: false`), so the
|
|
92195
|
+
* settable paths are the home-directory ones regardless of `global`.
|
|
92196
|
+
*
|
|
92197
|
+
* @see https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/run-state.ts
|
|
92198
|
+
* @see https://github.com/CodebuffAI/freebuff/blob/main/common/src/constants/knowledge.ts
|
|
92199
|
+
*/
|
|
92200
|
+
var CodebuffRule = class CodebuffRule extends ToolRule {
|
|
92201
|
+
static getSettablePaths(_options = {}) {
|
|
92202
|
+
return { root: {
|
|
92203
|
+
relativeDirPath: ".",
|
|
92204
|
+
relativeFilePath: CODEBUFF_GLOBAL_RULE_FILE_NAME
|
|
92205
|
+
} };
|
|
92206
|
+
}
|
|
92207
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
92208
|
+
const { root } = this.getSettablePaths({ global: true });
|
|
92209
|
+
if (relativeFilePath !== root.relativeFilePath) throw new Error(`CodebuffRule only supports the root rule: ${relativeFilePath}`);
|
|
92210
|
+
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
92211
|
+
return new CodebuffRule({
|
|
92212
|
+
outputRoot,
|
|
92213
|
+
relativeDirPath: root.relativeDirPath,
|
|
92214
|
+
relativeFilePath: root.relativeFilePath,
|
|
92215
|
+
fileContent,
|
|
92216
|
+
validate,
|
|
92217
|
+
root: true
|
|
92218
|
+
});
|
|
92219
|
+
}
|
|
92220
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
92221
|
+
const { root } = this.getSettablePaths({ global: true });
|
|
92222
|
+
const frontmatter = rulesyncRule.getFrontmatter();
|
|
92223
|
+
return new CodebuffRule({
|
|
92224
|
+
outputRoot,
|
|
92225
|
+
relativeDirPath: root.relativeDirPath,
|
|
92226
|
+
relativeFilePath: root.relativeFilePath,
|
|
92227
|
+
fileContent: rulesyncRule.getBody(),
|
|
92228
|
+
validate,
|
|
92229
|
+
root: frontmatter.root ?? false,
|
|
92230
|
+
description: frontmatter.description,
|
|
92231
|
+
globs: frontmatter.globs
|
|
92232
|
+
});
|
|
92233
|
+
}
|
|
92234
|
+
toRulesyncRule() {
|
|
92235
|
+
return this.toRulesyncRuleDefault();
|
|
92236
|
+
}
|
|
92237
|
+
validate() {
|
|
92238
|
+
return {
|
|
92239
|
+
success: true,
|
|
92240
|
+
error: null
|
|
92241
|
+
};
|
|
92242
|
+
}
|
|
92243
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
92244
|
+
const { root } = this.getSettablePaths({ global: true });
|
|
92245
|
+
return new CodebuffRule({
|
|
92246
|
+
outputRoot,
|
|
92247
|
+
relativeDirPath,
|
|
92248
|
+
relativeFilePath,
|
|
92249
|
+
fileContent: "",
|
|
92250
|
+
validate: false,
|
|
92251
|
+
root: relativeDirPath === root.relativeDirPath && relativeFilePath === root.relativeFilePath
|
|
92252
|
+
});
|
|
92253
|
+
}
|
|
92254
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
92255
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
92256
|
+
rulesyncRule,
|
|
92257
|
+
toolTarget: "codebuff"
|
|
92258
|
+
});
|
|
92259
|
+
}
|
|
92260
|
+
};
|
|
92261
|
+
//#endregion
|
|
91743
92262
|
//#region src/features/rules/codewhale-rule.ts
|
|
91744
92263
|
/**
|
|
91745
92264
|
* Rule generator for Codewhale.
|
|
@@ -93377,7 +93896,7 @@ var DshRule = class extends NestedAgentsmdRule {
|
|
|
93377
93896
|
* `getExtraFixedFiles`): Droid loads "direct `.md` children of each
|
|
93378
93897
|
* `output-styles` directory".
|
|
93379
93898
|
*/
|
|
93380
|
-
const OUTPUT_STYLE_FILE_GLOB = "*.md";
|
|
93899
|
+
const OUTPUT_STYLE_FILE_GLOB$1 = "*.md";
|
|
93381
93900
|
/**
|
|
93382
93901
|
* Rule generator for Factory Droid.
|
|
93383
93902
|
*
|
|
@@ -93499,7 +94018,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
93499
94018
|
static getExtraFixedFiles({ global = false } = {}) {
|
|
93500
94019
|
return [...this.getChannelPaths({ global }).map(({ path }) => path), {
|
|
93501
94020
|
relativeDirPath: this.getOutputStylesDirPath(),
|
|
93502
|
-
relativeFilePath: OUTPUT_STYLE_FILE_GLOB,
|
|
94021
|
+
relativeFilePath: OUTPUT_STYLE_FILE_GLOB$1,
|
|
93503
94022
|
importOnly: true
|
|
93504
94023
|
}];
|
|
93505
94024
|
}
|
|
@@ -95354,6 +95873,12 @@ const UNIVERSAL_GLOBS$1 = /* @__PURE__ */ new Set([
|
|
|
95354
95873
|
* Any other mode is kept in the `qoder` section so it survives a round trip.
|
|
95355
95874
|
*/
|
|
95356
95875
|
const INFERABLE_TRIGGERS = /* @__PURE__ */ new Set(["always_on", "glob"]);
|
|
95876
|
+
/**
|
|
95877
|
+
* The file-name glob `getExtraFixedFiles` returns for the output-styles
|
|
95878
|
+
* directory, so import enumerates every style in it (never deletion: the
|
|
95879
|
+
* directory also holds hand-written styles).
|
|
95880
|
+
*/
|
|
95881
|
+
const OUTPUT_STYLE_FILE_GLOB = "*.md";
|
|
95357
95882
|
function normalizeGlobList(value) {
|
|
95358
95883
|
if (value === void 0) return [];
|
|
95359
95884
|
return (typeof value === "string" ? splitBraceAwareList(value) : value).map((glob) => glob.trim()).filter((glob) => glob !== "");
|
|
@@ -95387,22 +95912,59 @@ function resolveTrigger(frontmatter) {
|
|
|
95387
95912
|
* A `qoder.trigger` (for example `model_decision` or `manual`) always takes
|
|
95388
95913
|
* precedence and round-trips through import.
|
|
95389
95914
|
*
|
|
95915
|
+
* A non-root rule with `qoder.channel: output-style` becomes a custom output
|
|
95916
|
+
* style instead, `.qoder/output-styles/<rule file name>.md` (both scopes),
|
|
95917
|
+
* with the rule's `description` and optional `qoder.name` as the style's
|
|
95918
|
+
* `description` / `name` frontmatter.
|
|
95919
|
+
*
|
|
95390
95920
|
* @see https://docs.qoder.com/en/cli/04-extending-qoder-cli/memory
|
|
95921
|
+
* @see https://docs.qoder.com/cli/output-styles
|
|
95391
95922
|
*/
|
|
95392
95923
|
var QoderRule = class QoderRule extends ToolRule {
|
|
95393
95924
|
frontmatter;
|
|
95394
95925
|
body;
|
|
95395
|
-
|
|
95926
|
+
outputStyle;
|
|
95927
|
+
constructor({ frontmatter, body, outputStyle = false, ...rest }) {
|
|
95396
95928
|
if (rest.validate) {
|
|
95397
95929
|
const result = QoderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
95398
95930
|
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
95399
95931
|
}
|
|
95400
95932
|
super({
|
|
95401
95933
|
...rest,
|
|
95402
|
-
fileContent: rest.root ? body : stringifyFrontmatter(body, frontmatter)
|
|
95934
|
+
fileContent: rest.root || outputStyle && Object.keys(frontmatter).length === 0 ? body : stringifyFrontmatter(body, frontmatter)
|
|
95403
95935
|
});
|
|
95404
95936
|
this.frontmatter = frontmatter;
|
|
95405
95937
|
this.body = body;
|
|
95938
|
+
this.outputStyle = outputStyle;
|
|
95939
|
+
}
|
|
95940
|
+
/**
|
|
95941
|
+
* Whether the given path is a custom output style: a direct `.md` child of
|
|
95942
|
+
* `.qoder/output-styles/`, at either scope.
|
|
95943
|
+
*/
|
|
95944
|
+
static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
|
|
95945
|
+
return relativeDirPath === QODER_OUTPUT_STYLES_DIR_PATH && extname(relativeFilePath) === ".md" && basename(relativeFilePath) === relativeFilePath;
|
|
95946
|
+
}
|
|
95947
|
+
/** Whether a rule opts into the `output-style` channel (non-root only). */
|
|
95948
|
+
static isOutputStyleRule(rulesyncRule) {
|
|
95949
|
+
const frontmatter = rulesyncRule.getFrontmatter();
|
|
95950
|
+
return !frontmatter.root && frontmatter.qoder?.channel === "output-style";
|
|
95951
|
+
}
|
|
95952
|
+
/**
|
|
95953
|
+
* The output-styles directory, enumerated for import only: it also holds
|
|
95954
|
+
* styles users write by hand (including personal ones in
|
|
95955
|
+
* `~/.qoder/output-styles/`) that rulesync cannot tell from its own, so
|
|
95956
|
+
* `--delete` leaves it alone.
|
|
95957
|
+
*/
|
|
95958
|
+
static getExtraFixedFiles(_params = {}) {
|
|
95959
|
+
return [{
|
|
95960
|
+
relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
95961
|
+
relativeFilePath: OUTPUT_STYLE_FILE_GLOB,
|
|
95962
|
+
importOnly: true
|
|
95963
|
+
}];
|
|
95964
|
+
}
|
|
95965
|
+
/** Qoder loads output styles itself; they are not rules to reference. */
|
|
95966
|
+
isExcludedFromRootReferences() {
|
|
95967
|
+
return this.outputStyle;
|
|
95406
95968
|
}
|
|
95407
95969
|
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
95408
95970
|
return {
|
|
@@ -95415,6 +95977,23 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
95415
95977
|
}
|
|
95416
95978
|
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
95417
95979
|
const paths = this.getSettablePaths({ global });
|
|
95980
|
+
if (this.isOutputStylePath({
|
|
95981
|
+
relativeDirPath,
|
|
95982
|
+
relativeFilePath
|
|
95983
|
+
})) {
|
|
95984
|
+
const filePath = join(outputRoot, QODER_OUTPUT_STYLES_DIR_PATH, relativeFilePath);
|
|
95985
|
+
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
95986
|
+
return new QoderRule({
|
|
95987
|
+
outputRoot,
|
|
95988
|
+
relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
95989
|
+
relativeFilePath,
|
|
95990
|
+
frontmatter,
|
|
95991
|
+
body: body.trim(),
|
|
95992
|
+
validate,
|
|
95993
|
+
root: false,
|
|
95994
|
+
outputStyle: true
|
|
95995
|
+
});
|
|
95996
|
+
}
|
|
95418
95997
|
if (relativeDirPath !== void 0 ? relativeDirPath !== QODER_RULES_DIR_PATH : relativeFilePath === paths.root.relativeFilePath) {
|
|
95419
95998
|
const rootDirPath = relativeDirPath ?? paths.root.relativeDirPath;
|
|
95420
95999
|
const fileContent = await readFileContent(join(outputRoot, rootDirPath, paths.root.relativeFilePath));
|
|
@@ -95447,6 +96026,11 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
95447
96026
|
const root = rulesyncFrontmatter.root ?? false;
|
|
95448
96027
|
const paths = this.getSettablePaths({ global });
|
|
95449
96028
|
const body = rulesyncRule.getBody();
|
|
96029
|
+
if (this.isOutputStyleRule(rulesyncRule)) return this.buildOutputStyle({
|
|
96030
|
+
outputRoot,
|
|
96031
|
+
rulesyncRule,
|
|
96032
|
+
validate
|
|
96033
|
+
});
|
|
95450
96034
|
if (root) return new QoderRule({
|
|
95451
96035
|
outputRoot,
|
|
95452
96036
|
relativeDirPath: paths.root.relativeDirPath,
|
|
@@ -95466,8 +96050,32 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
95466
96050
|
root: false
|
|
95467
96051
|
});
|
|
95468
96052
|
}
|
|
96053
|
+
/**
|
|
96054
|
+
* Write an `output-style` rule as `.qoder/output-styles/<name>.md`. The file
|
|
96055
|
+
* name is the rule's own, so a rule nested under `.rulesync/rules/` is
|
|
96056
|
+
* refused: Qoder reads only the files directly in `output-styles/`.
|
|
96057
|
+
*/
|
|
96058
|
+
static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
|
|
96059
|
+
const { description, qoder } = rulesyncRule.getFrontmatter();
|
|
96060
|
+
const sourcePath = rulesyncRule.getRelativeFilePath();
|
|
96061
|
+
if (basename(sourcePath) !== sourcePath) throw new Error(`Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
|
|
96062
|
+
const name = qoder?.name;
|
|
96063
|
+
return new QoderRule({
|
|
96064
|
+
outputRoot,
|
|
96065
|
+
relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
96066
|
+
relativeFilePath: `${basename(sourcePath, extname(sourcePath))}.md`,
|
|
96067
|
+
frontmatter: {
|
|
96068
|
+
...name !== void 0 && { name },
|
|
96069
|
+
...description !== void 0 && { description }
|
|
96070
|
+
},
|
|
96071
|
+
body: rulesyncRule.getBody(),
|
|
96072
|
+
validate,
|
|
96073
|
+
root: false,
|
|
96074
|
+
outputStyle: true
|
|
96075
|
+
});
|
|
96076
|
+
}
|
|
95469
96077
|
static buildFrontmatter(rulesyncFrontmatter, relativeFilePath) {
|
|
95470
|
-
const { trigger: storedTrigger, glob: storedGlob, ...section } = rulesyncFrontmatter.qoder ?? {};
|
|
96078
|
+
const { trigger: storedTrigger, glob: storedGlob, channel: _channel, name: _name, ...section } = rulesyncFrontmatter.qoder ?? {};
|
|
95471
96079
|
const specificGlobs = normalizeGlobList(storedGlob ?? rulesyncFrontmatter.globs).filter((glob) => !UNIVERSAL_GLOBS$1.has(glob));
|
|
95472
96080
|
let trigger = storedTrigger ?? (specificGlobs.length > 0 ? "glob" : "always_on");
|
|
95473
96081
|
const description = section.description ?? rulesyncFrontmatter.description;
|
|
@@ -95485,6 +96093,25 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
95485
96093
|
}
|
|
95486
96094
|
toRulesyncRule() {
|
|
95487
96095
|
if (this.isRoot()) return this.toRulesyncRuleDefault();
|
|
96096
|
+
if (this.outputStyle) {
|
|
96097
|
+
const { name, description } = this.frontmatter;
|
|
96098
|
+
return new RulesyncRule({
|
|
96099
|
+
outputRoot: this.getOutputRoot(),
|
|
96100
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
96101
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
96102
|
+
frontmatter: {
|
|
96103
|
+
root: false,
|
|
96104
|
+
targets: ["qoder"],
|
|
96105
|
+
...typeof description === "string" && { description },
|
|
96106
|
+
qoder: {
|
|
96107
|
+
channel: "output-style",
|
|
96108
|
+
...typeof name === "string" && name.trim() !== "" && { name }
|
|
96109
|
+
}
|
|
96110
|
+
},
|
|
96111
|
+
body: this.body,
|
|
96112
|
+
validate: true
|
|
96113
|
+
});
|
|
96114
|
+
}
|
|
95488
96115
|
const trigger = resolveTrigger(this.frontmatter);
|
|
95489
96116
|
const { trigger: _trigger, alwaysApply: _alwaysApply, description, glob, paths, ...unmapped } = this.frontmatter;
|
|
95490
96117
|
const fileGlobs = normalizeGlobList(glob ?? paths);
|
|
@@ -95533,6 +96160,10 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
95533
96160
|
return this.body;
|
|
95534
96161
|
}
|
|
95535
96162
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
96163
|
+
const outputStyle = this.isOutputStylePath({
|
|
96164
|
+
relativeDirPath,
|
|
96165
|
+
relativeFilePath
|
|
96166
|
+
});
|
|
95536
96167
|
return new QoderRule({
|
|
95537
96168
|
outputRoot,
|
|
95538
96169
|
relativeDirPath,
|
|
@@ -95540,7 +96171,8 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
95540
96171
|
frontmatter: {},
|
|
95541
96172
|
body: "",
|
|
95542
96173
|
validate: false,
|
|
95543
|
-
root: relativeDirPath !== QODER_RULES_DIR_PATH && relativeFilePath === "AGENTS.md"
|
|
96174
|
+
root: !outputStyle && relativeDirPath !== QODER_RULES_DIR_PATH && relativeFilePath === "AGENTS.md",
|
|
96175
|
+
outputStyle
|
|
95544
96176
|
});
|
|
95545
96177
|
}
|
|
95546
96178
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
@@ -97227,6 +97859,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
97227
97859
|
localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
|
|
97228
97860
|
}
|
|
97229
97861
|
}],
|
|
97862
|
+
["codebuff", {
|
|
97863
|
+
class: CodebuffRule,
|
|
97864
|
+
meta: {
|
|
97865
|
+
extension: "md",
|
|
97866
|
+
supportsProject: false,
|
|
97867
|
+
supportsGlobal: true,
|
|
97868
|
+
ruleDiscoveryMode: "auto",
|
|
97869
|
+
collisionPolicy: "fold"
|
|
97870
|
+
}
|
|
97871
|
+
}],
|
|
97230
97872
|
["codewhale", {
|
|
97231
97873
|
class: CodewhaleRule,
|
|
97232
97874
|
meta: {
|
|
@@ -97657,7 +98299,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
97657
98299
|
}]
|
|
97658
98300
|
]);
|
|
97659
98301
|
const allToolTargetKeys = [...toolRuleFactories.keys()];
|
|
97660
|
-
const rulesProcessorToolTargets = allToolTargetKeys
|
|
98302
|
+
const rulesProcessorToolTargets = allToolTargetKeys.filter((target) => {
|
|
98303
|
+
return toolRuleFactories.get(target)?.meta.supportsProject !== false;
|
|
98304
|
+
});
|
|
97661
98305
|
const rulesProcessorToolTargetsGlobal = allToolTargetKeys.filter((target) => {
|
|
97662
98306
|
return toolRuleFactories.get(target)?.meta.supportsGlobal ?? false;
|
|
97663
98307
|
});
|
|
@@ -97736,7 +98380,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
97736
98380
|
const factory = this.getFactory(this.toolTarget);
|
|
97737
98381
|
const { meta } = factory;
|
|
97738
98382
|
this.warnForIncludeRootOption(meta);
|
|
97739
|
-
this.
|
|
98383
|
+
this.warnForOutputStyleRuleOptions({
|
|
97740
98384
|
rules: alignedRules,
|
|
97741
98385
|
factory
|
|
97742
98386
|
});
|
|
@@ -97970,22 +98614,25 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
97970
98614
|
}) !== "auto") this.logger.warn("The rules option `ruleDiscoveryMode` has no effect with `includeRoot: false` on claudecode: the rule references it adds are written into CLAUDE.md, which is not generated.");
|
|
97971
98615
|
}
|
|
97972
98616
|
/**
|
|
97973
|
-
* Warn about `factorydroid` frontmatter that does not do what it
|
|
97974
|
-
* it does: an `output-style` rule that also targets other tools
|
|
97975
|
-
* as an ordinary rule (so presentation instructions end up in
|
|
97976
|
-
* rules), and
|
|
97977
|
-
* Checked while generating for
|
|
98617
|
+
* Warn about `factorydroid` / `qoder` frontmatter that does not do what it
|
|
98618
|
+
* looks like it does: an `output-style` rule that also targets other tools
|
|
98619
|
+
* reaches them as an ordinary rule (so presentation instructions end up in
|
|
98620
|
+
* their project rules), and `<tool>.name` is read only with
|
|
98621
|
+
* `channel: output-style`. Checked while generating for that tool only, so
|
|
98622
|
+
* each warning prints once.
|
|
97978
98623
|
*/
|
|
97979
|
-
|
|
97980
|
-
|
|
98624
|
+
warnForOutputStyleRuleOptions({ rules, factory }) {
|
|
98625
|
+
const toolTarget = this.toolTarget;
|
|
98626
|
+
if (toolTarget !== "factorydroid" && toolTarget !== "qoder") return;
|
|
97981
98627
|
for (const rule of rules) {
|
|
97982
98628
|
const frontmatter = rule.getFrontmatter();
|
|
97983
|
-
|
|
97984
|
-
|
|
98629
|
+
const section = frontmatter[toolTarget];
|
|
98630
|
+
if (frontmatter.root || !section || !factory.class.isTargetedByRulesyncRule(rule)) continue;
|
|
98631
|
+
const { channel, name } = section;
|
|
97985
98632
|
const fileName = stripControlCharacters(rule.getRelativeFilePath());
|
|
97986
98633
|
const targets = frontmatter.targets ?? ["*"];
|
|
97987
|
-
if (channel === "output-style" && targets.some((target) => target !==
|
|
97988
|
-
if (channel !== "output-style" && name !== void 0) this.logger.warn(`${fileName} sets
|
|
98634
|
+
if (channel === "output-style" && targets.some((target) => target !== toolTarget)) this.logger.warn(`${fileName} sets ${toolTarget}.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["${toolTarget}"]\` to keep the output style to ${toolTarget === "qoder" ? "Qoder" : "Factory Droid"}.`);
|
|
98635
|
+
if (channel !== "output-style" && name !== void 0) this.logger.warn(`${fileName} sets ${toolTarget}.name, which is read only with ${toolTarget}.channel: output-style; ignoring it.`);
|
|
97989
98636
|
}
|
|
97990
98637
|
}
|
|
97991
98638
|
/**
|
|
@@ -101730,6 +102377,6 @@ async function importChecksCore(params) {
|
|
|
101730
102377
|
return writtenCount;
|
|
101731
102378
|
}
|
|
101732
102379
|
//#endregion
|
|
101733
|
-
export { RulesyncHooks as $,
|
|
102380
|
+
export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, ALL_FEATURES as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_MCP_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, hasDeceptiveHiddenCharacters as Gn, isSymlink as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, DEPRECATED_FEATURE_REPLACEMENTS as Hn, hasExecutableBit as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as In, directoryExists as It, RulesyncRule as J, stripControlCharacters as Jn, listSubdirectoryNames as Jt, RulesyncSkill as K, hasEnclosingMarkOutsideKeycap as Kn, listDirectoryEntryNames as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_USER_CONFIG_DIR_NAME as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RULES_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_SCHEMA_URL as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, formatError as Un, isFileNotFoundError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES_WITH_WILDCARD as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, truncateText as Wn, isFileSystemError as Wt, RulesyncPermissions as X, stripHiddenCharacters as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharactersKeepingLineFeeds as Yn, pathEscapesRoot as Yt, RulesyncMcp as Z, readFileContentOrNull as Zt, LETTACODE_DIR as _, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CONFIG_RELATIVE_FILE_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS_WITH_WILDCARD as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, PACKAGING_TOOL_TARGETS as fn, mergeInputRootConfigs as ft, HooksProcessor as g, MAX_FILE_SIZE as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, FEATURE_SOURCE_TREE_ENTRIES as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedPathEscapesRoot as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_SCHEMA_URL as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, CURATED_RULES_FEATURE_SUBDIR as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, ToolTargetSchema as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, quoteForLog as qn, listFilePathsRecursively as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, ALL_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CHECKS_RELATIVE_DIR_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_HOOKS_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CONFIG_SCHEMA_URL as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, parseCommaSeparatedList as zn, findFilesByGlobs as zt };
|
|
101734
102381
|
|
|
101735
|
-
//# sourceMappingURL=import-
|
|
102382
|
+
//# sourceMappingURL=import-DFwX4TTE.js.map
|