rulesync 37.1.0 → 38.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.
@@ -630,6 +630,7 @@ const mcpProcessorToolTargetTuple = [
630
630
  "musecode",
631
631
  "mimocode",
632
632
  "omp",
633
+ "openclaw",
633
634
  "opencode",
634
635
  "pi",
635
636
  "pool",
@@ -682,6 +683,7 @@ const commandsProcessorToolTargetTuple = [
682
683
  "kiro",
683
684
  "kiro-cli",
684
685
  "kiro-ide",
686
+ "lettacode",
685
687
  "mimocode",
686
688
  "omp",
687
689
  "opencode",
@@ -909,6 +911,7 @@ const permissionsProcessorToolTargetTuple = [
909
911
  "rovodev",
910
912
  "tabnine",
911
913
  "takt",
914
+ "trae",
912
915
  "vibe",
913
916
  "warp",
914
917
  "warpcli",
@@ -4742,10 +4745,13 @@ const CLAUDE_HOOK_EVENTS = [
4742
4745
  * eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
4743
4746
  * `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
4744
4747
  * `PostCompaction` (fires after context compaction; added in the Devin CLI
4745
- * stable changelog — https://docs.devin.ai/cli/changelog/stable). The
4746
- * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
4747
- * a `matcher` (regex against `tool_name`); the session/turn/compaction events
4748
- * do not.
4748
+ * stable changelog — https://docs.devin.ai/cli/changelog/stable). CLI
4749
+ * v3000.11.92 added `PreCompact` and `PostCompact`, which run before and after
4750
+ * compaction with `manual` / `auto` trigger matchers; `PostCompaction` stays
4751
+ * supported. The tool/permission events (`PreToolUse`/`PostToolUse`/
4752
+ * `PermissionRequest`) carry a `matcher` (regex against `tool_name`) and
4753
+ * `PreCompact` a trigger matcher; the other session/turn/compaction events do
4754
+ * not.
4749
4755
  *
4750
4756
  * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
4751
4757
  * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
@@ -4761,6 +4767,7 @@ const DEVIN_HOOK_EVENTS = [
4761
4767
  "beforeSubmitPrompt",
4762
4768
  "stop",
4763
4769
  "permissionRequest",
4770
+ "preCompact",
4764
4771
  "postCompact"
4765
4772
  ];
4766
4773
  /**
@@ -6144,12 +6151,17 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6144
6151
  beforeSubmitPrompt: "UserPromptSubmit",
6145
6152
  stop: "Stop",
6146
6153
  permissionRequest: "PermissionRequest",
6154
+ preCompact: "PreCompact",
6147
6155
  postCompact: "PostCompaction"
6148
6156
  };
6149
6157
  /**
6150
- * Map Devin Local PascalCase event names to canonical camelCase.
6158
+ * Map Devin Local PascalCase event names to canonical camelCase. `PostCompact`
6159
+ * (CLI v3000.11.92) is accepted on import as an alias of `PostCompaction`.
6151
6160
  */
6152
- const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
6161
+ const DEVIN_TO_CANONICAL_EVENT_NAMES = {
6162
+ ...Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k])),
6163
+ PostCompact: "postCompact"
6164
+ };
6153
6165
  /**
6154
6166
  * Map canonical camelCase event names to AugmentCode PascalCase.
6155
6167
  * Auggie reuses the same PascalCase names as Claude for the events it supports.
@@ -7082,6 +7094,17 @@ const CodexcliMcpToolConfigSchema = z.looseObject({
7082
7094
  function isCodexcliToolConfig(value) {
7083
7095
  return CodexcliMcpToolConfigSchema.safeParse(value).success;
7084
7096
  }
7097
+ const CrushChannelReplyRouteSchema = z.looseObject({
7098
+ tool: z.string(),
7099
+ target_param: z.optional(z.string()),
7100
+ target_meta: z.optional(z.string())
7101
+ });
7102
+ const CrushChannelReplySchema = z.looseObject({
7103
+ user: z.optional(CrushChannelReplyRouteSchema),
7104
+ group: z.optional(CrushChannelReplyRouteSchema),
7105
+ message_param: z.optional(z.string()),
7106
+ suppress_tools: z.optional(z.array(z.string()))
7107
+ });
7085
7108
  const McpServerSchema = z.looseObject({
7086
7109
  type: z.optional(z.enum([
7087
7110
  "local",
@@ -7123,6 +7146,9 @@ const McpServerSchema = z.looseObject({
7123
7146
  crushOauthClientSecret: z.optional(z.string()),
7124
7147
  crushOauthCallbackPort: z.optional(z.int()),
7125
7148
  crushSessionless: z.optional(z.boolean()),
7149
+ crushChannelEnabled: z.optional(z.boolean()),
7150
+ crushChannelReply: z.optional(CrushChannelReplySchema),
7151
+ zcodeOauth: z.optional(z.looseObject({ type: z.string() })),
7126
7152
  poolAllow: z.optional(z.array(z.string())),
7127
7153
  headers: z.optional(z.record(z.string(), z.string())),
7128
7154
  /**
@@ -7234,6 +7260,7 @@ const RulesyncMcpFileSchema = z.looseObject({
7234
7260
  kiro: z.optional(toolScopedMcpSchema),
7235
7261
  mimocode: z.optional(toolScopedMcpSchema),
7236
7262
  omp: z.optional(toolScopedMcpSchema),
7263
+ openclaw: z.optional(toolScopedMcpSchema),
7237
7264
  opencode: z.optional(toolScopedMcpSchema),
7238
7265
  pi: z.optional(toolScopedMcpSchema),
7239
7266
  qoder: z.optional(toolScopedMcpSchema),
@@ -7581,11 +7608,16 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
7581
7608
  "crushOauthClientSecret",
7582
7609
  "crushOauthCallbackPort",
7583
7610
  "crushSessionless",
7611
+ "crushChannelEnabled",
7612
+ "crushChannelReply",
7584
7613
  "poolAllow",
7614
+ "zcodeOauth",
7585
7615
  "oauth_client_id",
7586
7616
  "oauth_client_secret",
7587
7617
  "oauth_callback_port",
7588
7618
  "sessionless",
7619
+ "channel_enabled",
7620
+ "channel_reply",
7589
7621
  "enabled"
7590
7622
  ])];
7591
7623
  }));
@@ -8321,8 +8353,12 @@ const TabninePermissionsOverrideSchema = z.looseObject({
8321
8353
  * are merged into the matching `reasonix.toml` table and emitted only for
8322
8354
  * Reasonix, while the shared `permission` block continues to drive
8323
8355
  * `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
8324
- * Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
8325
- * re-extracted from `[agent]` on import — other `agent` keys authored here reach
8356
+ * The user-only `[sandbox]` keys (`allow_devices`, `egress_unlisted`,
8357
+ * `host_authorities`) are written only by `--global` generate: Reasonix replaces
8358
+ * a project value with the user's (`narrowSandbox` in `project_scope.go`).
8359
+ * Note: the whole `[sandbox]` table round-trips (minus the user-only keys of a
8360
+ * project file), but only the plan-mode keys are re-extracted from `[agent]` on
8361
+ * import — other `agent` keys authored here reach
8326
8362
  * `reasonix.toml` on generate but are not re-extracted back into the override.
8327
8363
  *
8328
8364
  * @example
@@ -8354,14 +8390,52 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
8354
8390
  * output-style` rule option) or a `plugin-name:style-name` reference. The
8355
8391
  * shared `permission` block continues to drive `permissions.allow`/`ask`/`deny`.
8356
8392
  *
8393
+ * The other documented settings with no canonical category are authored here
8394
+ * too and merged into the same file, key by key, over what the file already
8395
+ * holds: `general.defaultPermissionMode`, `tools.core` / `tools.exclude`, the
8396
+ * MCP approval and loading keys under `mcp` (`allowed`, `excluded`,
8397
+ * `enableAllProjectMcpServers`, `enabledProjectMcpServers`, `lazyLoad`),
8398
+ * `agents.overrides`, the top-level `agentsMdExcludes` and the top-level
8399
+ * `autoMode` classifier guidance (`allow` / `soft_deny` / `environment`).
8400
+ * Qoder reads `autoMode` from the user settings only, so it is written by
8401
+ * `--global` generate alone. Only the keys listed here are written; any other
8402
+ * key of these groups or of `autoMode` (`tools.shell.pager`,
8403
+ * `general.preferredEditor`, ...) is skipped with a warning. Each authored key
8404
+ * is laid over what the file holds; removing it from the override leaves the
8405
+ * previously written value in place.
8406
+ *
8357
8407
  * @see https://docs.qoder.com/cli/output-styles
8358
8408
  * @see https://docs.qoder.com/cli/settings-reference
8409
+ * @see https://docs.qoder.com/en/cli/permissions
8359
8410
  * @example
8360
8411
  * { "outputStyle": "concise-review" }
8412
+ * @example
8413
+ * { "mcp": { "enabledProjectMcpServers": ["playwright"], "lazyLoad": true },
8414
+ * "tools": { "exclude": ["WebSearch"] },
8415
+ * "autoMode": { "environment": ["This is a pnpm monorepo"] } }
8361
8416
  */
8362
8417
  const QoderPermissionsOverrideSchema = z.looseObject({
8363
8418
  permission: z.optional(ToolScopedPermissionSchema),
8364
- outputStyle: z.optional(z.string().check(z.trim(), z.minLength(1)))
8419
+ outputStyle: z.optional(z.string().check(z.trim(), z.minLength(1))),
8420
+ general: z.optional(z.looseObject({ defaultPermissionMode: z.optional(z.string().check(z.trim(), z.minLength(1))) })),
8421
+ tools: z.optional(z.looseObject({
8422
+ core: z.optional(z.array(z.string())),
8423
+ exclude: z.optional(z.array(z.string()))
8424
+ })),
8425
+ mcp: z.optional(z.looseObject({
8426
+ allowed: z.optional(z.array(z.string())),
8427
+ excluded: z.optional(z.array(z.string())),
8428
+ enableAllProjectMcpServers: z.optional(z.boolean()),
8429
+ enabledProjectMcpServers: z.optional(z.array(z.string())),
8430
+ lazyLoad: z.optional(z.boolean())
8431
+ })),
8432
+ agents: z.optional(z.looseObject({ overrides: z.optional(z.record(z.string(), z.looseObject({}))) })),
8433
+ autoMode: z.optional(z.looseObject({
8434
+ allow: z.optional(z.array(z.string())),
8435
+ soft_deny: z.optional(z.array(z.string())),
8436
+ environment: z.optional(z.array(z.string()))
8437
+ })),
8438
+ agentsMdExcludes: z.optional(z.array(z.string()))
8365
8439
  });
8366
8440
  /**
8367
8441
  * One native Factory Droid permission rule. `match.prefix` positions are either
@@ -9149,14 +9223,40 @@ const ZedPermissionsOverrideSchema = z.looseObject({
9149
9223
  *
9150
9224
  * @example
9151
9225
  * { "sandbox": { "allowed_domains": ["github.com"], "network_mode": "limited" } }
9226
+ * The same override carries the repo-level plugin governance lists Devin reads
9227
+ * from a checkout's `.devin/config.json`: `requiredPlugins` (plugin sources
9228
+ * installed automatically), `optionalPlugins` (endorsed sources that carve an
9229
+ * exception out of a forbidden entry; never installed) and `forbiddenPlugins`
9230
+ * (plugin identities or `*` globs that may not load). A source is a string
9231
+ * (`"owner/repo"` or a git URL) or an object such as
9232
+ * `{ "source": "github", "repo": "owner/repo" }`. They are a repo governance
9233
+ * level, so they are emitted at project scope only.
9234
+ *
9152
9235
  * @example
9153
9236
  * { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
9237
+ * @example
9238
+ * { "forbiddenPlugins": ["*"], "optionalPlugins": ["acme/approved"] }
9154
9239
  * @see https://docs.devin.ai/cli/sandbox
9155
9240
  * @see https://docs.devin.ai/cli/reference/configuration/config-file
9241
+ * @see https://docs.devin.ai/cli/extensibility/plugins/overview
9242
+ */
9243
+ const DevinPluginSourceListSchema = z.array(z.union([z.string(), z.looseObject({})]));
9244
+ /**
9245
+ * Shapes of the Devin repo-level plugin governance lists, keyed by their
9246
+ * `.devin/config.json` name. Shared by the override schema and the import,
9247
+ * which drops a list that would not pass it.
9156
9248
  */
9249
+ const DEVIN_PLUGIN_GOVERNANCE_SCHEMAS = {
9250
+ requiredPlugins: DevinPluginSourceListSchema,
9251
+ optionalPlugins: DevinPluginSourceListSchema,
9252
+ forbiddenPlugins: z.array(z.string())
9253
+ };
9157
9254
  const DevinPermissionsOverrideSchema = z.looseObject({
9158
9255
  permission: z.optional(ToolScopedPermissionSchema),
9159
- sandbox: z.optional(z.looseObject({}))
9256
+ sandbox: z.optional(z.looseObject({})),
9257
+ requiredPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.requiredPlugins),
9258
+ optionalPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.optionalPlugins),
9259
+ forbiddenPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins)
9160
9260
  });
9161
9261
  /**
9162
9262
  * Permissions configuration.
@@ -9230,6 +9330,7 @@ const PermissionsConfigSchema = z.looseObject({
9230
9330
  "kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
9231
9331
  roo: z.optional(CanonicalPermissionsOverrideSchema),
9232
9332
  rovodev: z.optional(CanonicalPermissionsOverrideSchema),
9333
+ trae: z.optional(CanonicalPermissionsOverrideSchema),
9233
9334
  zoocode: z.optional(CanonicalPermissionsOverrideSchema)
9234
9335
  });
9235
9336
  /**
@@ -11088,6 +11189,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
11088
11189
  compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
11089
11190
  metadata: z.optional(z.looseObject({}))
11090
11191
  })),
11192
+ musecode: z.optional(z.looseObject({
11193
+ license: z.optional(z.string()),
11194
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
11195
+ metadata: z.optional(z.looseObject({}))
11196
+ })),
11091
11197
  zcode: z.optional(z.looseObject({
11092
11198
  when_to_use: z.optional(z.string()),
11093
11199
  license: z.optional(z.string()),
@@ -11518,8 +11624,20 @@ const GITLABDUO_MR_REVIEW_INSTRUCTIONS_FILE_NAME = "mr-review-instructions.yaml"
11518
11624
  const GITLABDUO_COMMANDS_DIR = join(".agents", "commands");
11519
11625
  /** User custom slash commands. */
11520
11626
  const GITLABDUO_GLOBAL_COMMANDS_DIR = join(GITLABDUO_DIR, "commands");
11521
- /** Project skills live in a non-hidden `skills/` directory at the project root. */
11522
- const GITLABDUO_SKILLS_DIR = "skills";
11627
+ /**
11628
+ * Project skills live in the cross-tool `.agents/skills/` directory, which
11629
+ * GitLab Duo reads alongside a non-hidden `skills/` directory at the project
11630
+ * root; when both hold a skill of the same name, `.agents/skills/` wins.
11631
+ *
11632
+ * @see https://docs.gitlab.com/user/duo_agent_platform/customize/agent_skills/
11633
+ */
11634
+ const GITLABDUO_SKILLS_DIR = join(".agents", "skills");
11635
+ /**
11636
+ * The project-root `skills/` directory GitLab Duo also reads. Earlier rulesync
11637
+ * versions wrote project skills there; it is now an import-only root, since it
11638
+ * routinely holds hand-authored skills rulesync must not manage.
11639
+ */
11640
+ const GITLABDUO_LEGACY_SKILLS_DIR = "skills";
11523
11641
  /** User skills (requires `--enable-global-skills` / `GITLAB_ENABLE_GLOBAL_SKILLS`). */
11524
11642
  const GITLABDUO_GLOBAL_SKILLS_DIR = join(GITLABDUO_DIR, "skills");
11525
11643
  //#endregion
@@ -11681,6 +11799,239 @@ const CODEXCLI_OVERRIDE_KEYS = [
11681
11799
  "approvals_reviewer",
11682
11800
  "tui"
11683
11801
  ];
11802
+ const SINGLE_CHAR_ESCAPES = {
11803
+ "'": "'",
11804
+ "\"": "\"",
11805
+ "\\": "\\",
11806
+ b: "\b",
11807
+ f: "\f",
11808
+ n: "\n",
11809
+ r: "\r",
11810
+ t: " ",
11811
+ v: "\v"
11812
+ };
11813
+ const LINE_TERMINATORS = /* @__PURE__ */ new Set([
11814
+ "\n",
11815
+ "\r",
11816
+ "\u2028",
11817
+ "\u2029"
11818
+ ]);
11819
+ const ID_START = /[\p{L}\p{Nl}$_]/u;
11820
+ const ID_CONTINUE = /[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$_\u200C\u200D]/u;
11821
+ const WHITESPACE$1 = /[\t\n\v\f\r \u00A0\u2028\u2029\uFEFF\p{Zs}]/u;
11822
+ var Json5Reader = class {
11823
+ text;
11824
+ pos = 0;
11825
+ constructor(text) {
11826
+ this.text = text;
11827
+ }
11828
+ parse() {
11829
+ this.skipTrivia();
11830
+ const value = this.readValue(0);
11831
+ this.skipTrivia();
11832
+ if (this.pos < this.text.length) this.fail(`unexpected ${this.describe()} after the end of the document`);
11833
+ return value;
11834
+ }
11835
+ fail(message) {
11836
+ let line = 1;
11837
+ let column = 1;
11838
+ for (let i = 0; i < this.pos && i < this.text.length; i++) if (this.text[i] === "\n") {
11839
+ line++;
11840
+ column = 1;
11841
+ } else column++;
11842
+ throw new SyntaxError(`JSON5: ${message} at line ${line}, column ${column}`);
11843
+ }
11844
+ describe() {
11845
+ const char = this.text[this.pos];
11846
+ return char === void 0 ? "end of input" : `character ${JSON.stringify(char)}`;
11847
+ }
11848
+ peek(offset = 0) {
11849
+ return this.text[this.pos + offset];
11850
+ }
11851
+ skipTrivia() {
11852
+ while (this.pos < this.text.length) {
11853
+ const char = this.text[this.pos];
11854
+ if (WHITESPACE$1.test(char)) this.pos++;
11855
+ else if (char === "/" && this.peek(1) === "/") {
11856
+ this.pos += 2;
11857
+ while (this.pos < this.text.length && !LINE_TERMINATORS.has(this.text[this.pos])) this.pos++;
11858
+ } else if (char === "/" && this.peek(1) === "*") {
11859
+ const end = this.text.indexOf("*/", this.pos + 2);
11860
+ if (end === -1) this.fail("unterminated block comment");
11861
+ this.pos = end + 2;
11862
+ } else return;
11863
+ }
11864
+ }
11865
+ readValue(depth) {
11866
+ const char = this.peek();
11867
+ if (char === "{" || char === "[") {
11868
+ if (depth >= 256) this.fail(`document nests deeper than 256 levels`);
11869
+ return char === "{" ? this.readObject(depth + 1) : this.readArray(depth + 1);
11870
+ }
11871
+ if (char === "\"" || char === "'") return this.readString();
11872
+ if (char !== void 0 && /[0-9+\-.IN]/.test(char)) return this.readNumber();
11873
+ for (const [literal, value] of [
11874
+ ["true", true],
11875
+ ["false", false],
11876
+ ["null", null]
11877
+ ]) if (this.text.startsWith(literal, this.pos) && !this.isIdContinueAt(literal.length)) {
11878
+ this.pos += literal.length;
11879
+ return value;
11880
+ }
11881
+ return this.fail(`unexpected ${this.describe()}`);
11882
+ }
11883
+ isIdContinueAt(offset) {
11884
+ const char = this.peek(offset);
11885
+ return char !== void 0 && (ID_CONTINUE.test(char) || char === "\\");
11886
+ }
11887
+ readObject(depth) {
11888
+ const result = {};
11889
+ this.pos++;
11890
+ this.skipTrivia();
11891
+ while (this.peek() !== "}") {
11892
+ const key = this.readKey();
11893
+ this.skipTrivia();
11894
+ if (this.peek() !== ":") this.fail(`expected ':' after the key ${JSON.stringify(key)}`);
11895
+ this.pos++;
11896
+ this.skipTrivia();
11897
+ const value = this.readValue(depth);
11898
+ Object.defineProperty(result, key, {
11899
+ value,
11900
+ enumerable: true,
11901
+ writable: true,
11902
+ configurable: true
11903
+ });
11904
+ this.skipTrivia();
11905
+ if (this.peek() === ",") {
11906
+ this.pos++;
11907
+ this.skipTrivia();
11908
+ continue;
11909
+ }
11910
+ if (this.peek() !== "}") this.fail(`expected ',' or '}' but found ${this.describe()}`);
11911
+ }
11912
+ this.pos++;
11913
+ return result;
11914
+ }
11915
+ readArray(depth) {
11916
+ const result = [];
11917
+ this.pos++;
11918
+ this.skipTrivia();
11919
+ while (this.peek() !== "]") {
11920
+ result.push(this.readValue(depth));
11921
+ this.skipTrivia();
11922
+ if (this.peek() === ",") {
11923
+ this.pos++;
11924
+ this.skipTrivia();
11925
+ continue;
11926
+ }
11927
+ if (this.peek() !== "]") this.fail(`expected ',' or ']' but found ${this.describe()}`);
11928
+ }
11929
+ this.pos++;
11930
+ return result;
11931
+ }
11932
+ readKey() {
11933
+ const char = this.peek();
11934
+ if (char === "\"" || char === "'") return this.readString();
11935
+ let key = "";
11936
+ while (this.pos < this.text.length) {
11937
+ const start = this.pos;
11938
+ let next = this.peek();
11939
+ if (next === "\\") {
11940
+ if (this.peek(1) !== "u") this.fail("expected a \\u escape in an identifier key");
11941
+ this.pos += 2;
11942
+ next = this.readHex(4);
11943
+ } else this.pos++;
11944
+ if (!(key === "" ? ID_START.test(next) : ID_CONTINUE.test(next))) {
11945
+ this.pos = start;
11946
+ break;
11947
+ }
11948
+ key += next;
11949
+ }
11950
+ if (key === "") this.fail(`expected a key but found ${this.describe()}`);
11951
+ return key;
11952
+ }
11953
+ /** Reads `length` hex digits at the cursor and returns the character they encode. */
11954
+ readHex(length) {
11955
+ const digits = this.text.slice(this.pos, this.pos + length);
11956
+ if (digits.length !== length || !/^[0-9a-fA-F]+$/.test(digits)) this.fail(`expected ${length} hexadecimal digits`);
11957
+ this.pos += length;
11958
+ return String.fromCharCode(Number.parseInt(digits, 16));
11959
+ }
11960
+ readString() {
11961
+ const quote = this.peek();
11962
+ this.pos++;
11963
+ let result = "";
11964
+ while (true) {
11965
+ const char = this.peek();
11966
+ if (char === void 0) this.fail("unterminated string");
11967
+ if (char === quote) {
11968
+ this.pos++;
11969
+ return result;
11970
+ }
11971
+ if (char === "\n" || char === "\r") this.fail("unescaped line break in a string");
11972
+ if (char !== "\\") {
11973
+ result += char;
11974
+ this.pos++;
11975
+ continue;
11976
+ }
11977
+ this.pos++;
11978
+ const escape = this.peek();
11979
+ if (escape === void 0) this.fail("unterminated string");
11980
+ if (escape === "\r") this.pos += this.peek(1) === "\n" ? 2 : 1;
11981
+ else if (LINE_TERMINATORS.has(escape)) this.pos++;
11982
+ else if (escape === "x") {
11983
+ this.pos++;
11984
+ result += this.readHex(2);
11985
+ } else if (escape === "u") {
11986
+ this.pos++;
11987
+ result += this.readHex(4);
11988
+ } else if (escape === "0") {
11989
+ if (/[0-9]/.test(this.peek(1) ?? "")) this.fail("octal escapes are not allowed");
11990
+ result += "\0";
11991
+ this.pos++;
11992
+ } else if (/[1-9]/.test(escape)) this.fail("octal escapes are not allowed");
11993
+ else {
11994
+ result += SINGLE_CHAR_ESCAPES[escape] ?? escape;
11995
+ this.pos++;
11996
+ }
11997
+ }
11998
+ }
11999
+ readNumber() {
12000
+ const start = this.pos;
12001
+ let sign = 1;
12002
+ if (this.peek() === "+" || this.peek() === "-") {
12003
+ sign = this.peek() === "-" ? -1 : 1;
12004
+ this.pos++;
12005
+ }
12006
+ for (const [literal, value] of [["Infinity", Infinity], ["NaN", NaN]]) if (this.text.startsWith(literal, this.pos)) {
12007
+ if (this.isIdContinueAt(literal.length)) break;
12008
+ this.pos += literal.length;
12009
+ return sign * value;
12010
+ }
12011
+ if (this.peek() === "0" && (this.peek(1) === "x" || this.peek(1) === "X")) {
12012
+ this.pos += 2;
12013
+ const digits = /^[0-9a-fA-F]+/.exec(this.text.slice(this.pos))?.[0];
12014
+ if (digits === void 0) this.fail("expected hexadecimal digits");
12015
+ this.pos += digits.length;
12016
+ return sign * Number.parseInt(digits, 16);
12017
+ }
12018
+ const match = /^(?:(?:0|[1-9][0-9]*)(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.pos));
12019
+ if (match === null) {
12020
+ this.pos = start;
12021
+ this.fail(`invalid number`);
12022
+ }
12023
+ this.pos += match[0].length;
12024
+ if (this.isIdContinueAt(0)) this.fail(`invalid number`);
12025
+ return sign * Number(match[0]);
12026
+ }
12027
+ };
12028
+ /**
12029
+ * Parse a JSON5 document. Throws a `SyntaxError` naming the line and column of
12030
+ * the first problem.
12031
+ */
12032
+ function parseJson5(text) {
12033
+ return new Json5Reader(text).parse();
12034
+ }
11684
12035
  //#endregion
11685
12036
  //#region src/utils/quote-value.ts
11686
12037
  /**
@@ -11830,6 +12181,7 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
11830
12181
  if (format === "yaml") parsed = loadYaml(fileContent);
11831
12182
  else if (format === "toml") parsed = parse$2(fileContent);
11832
12183
  else if (format === "json") parsed = JSON.parse(fileContent);
12184
+ else if (format === "json5") parsed = parseJson5(fileContent);
11833
12185
  else if (jsoncParseErrors === "error") {
11834
12186
  const errors = [];
11835
12187
  parsed = parse$1(fileContent, errors, { allowTrailingComma: true });
@@ -12427,8 +12779,12 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
12427
12779
  format,
12428
12780
  document
12429
12781
  });
12430
- if (format !== "jsonc" || existingContent.trim() === "") return whole;
12782
+ if (format !== "jsonc" && format !== "json5" || existingContent.trim() === "") return whole;
12431
12783
  const wholeBecause = (reason) => {
12784
+ if (format === "json5") throw new Json5EditRefusedError({
12785
+ filePath,
12786
+ reason
12787
+ });
12432
12788
  warnAboutWholeJsoncRewrite({
12433
12789
  filePath,
12434
12790
  reason,
@@ -12439,7 +12795,7 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
12439
12795
  try {
12440
12796
  const errors = [];
12441
12797
  const root = parseTree(existingContent, errors, { allowTrailingComma: true });
12442
- if (root === void 0 || errors.length > 0) return wholeBecause("it could not be parsed");
12798
+ if (root === void 0 || errors.length > 0) return wholeBecause(format === "json5" ? "it uses JSON5-only syntax (such as unquoted keys, single quotes, hex numbers, Infinity or NaN)" : "it could not be parsed");
12443
12799
  if (root.type !== "object") return wholeBecause("its root is not an object");
12444
12800
  const uneditable = uneditableKeyOf(root);
12445
12801
  if (uneditable !== void 0) {
@@ -12468,10 +12824,22 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
12468
12824
  leadingKeys
12469
12825
  });
12470
12826
  } catch (error) {
12827
+ if (error instanceof Json5EditRefusedError) throw error;
12471
12828
  return wholeBecause(`the editor refused it (${stripControlCharacters(formatError(error))})`);
12472
12829
  }
12473
12830
  }
12474
12831
  /**
12832
+ * Thrown instead of rewriting a `json5` file whole (see
12833
+ * {@link serializeSharedConfig}); nothing is written.
12834
+ */
12835
+ var Json5EditRefusedError = class extends Error {
12836
+ constructor({ filePath, reason }) {
12837
+ const subject = filePath === void 0 ? "a shared JSON5 config file" : quoteForLog(filePath);
12838
+ super(`Refusing to update ${subject}: rulesync cannot edit it in place because ${reason}, and rewriting it whole would drop its comments and could change its values. Convert it to JSONC-compatible syntax (double-quoted keys and strings, decimal numbers; comments and trailing commas are fine), or add the entries by hand.`);
12839
+ this.name = "Json5EditRefusedError";
12840
+ }
12841
+ };
12842
+ /**
12475
12843
  * Report, once per run and file, that a JSONC file is being written whole
12476
12844
  * instead of edited in place. Named by `filePath` when the caller knows it;
12477
12845
  * a caller serializing text it never read from disk has no name to give.
@@ -12534,6 +12902,7 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
12534
12902
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
12535
12903
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
12536
12904
  const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
12905
+ const OPENCLAW_CONFIG_SHARED_FILE_KEY = ".openclaw/openclaw.json";
12537
12906
  const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
12538
12907
  const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
12539
12908
  /**
@@ -12989,7 +13358,12 @@ const SHARED_CONFIG_OWNERSHIP = {
12989
13358
  format: "json",
12990
13359
  features: { permissions: {
12991
13360
  kind: "replace-owned-keys",
12992
- ownedKeys: ["permissions"]
13361
+ ownedKeys: [
13362
+ "permissions",
13363
+ "requiredPlugins",
13364
+ "optionalPlugins",
13365
+ "forbiddenPlugins"
13366
+ ]
12993
13367
  } }
12994
13368
  },
12995
13369
  ".config/devin/config.json": {
@@ -13137,10 +13511,27 @@ const SHARED_CONFIG_OWNERSHIP = {
13137
13511
  },
13138
13512
  permissions: {
13139
13513
  kind: "replace-owned-keys",
13140
- ownedKeys: ["permissions", "outputStyle"]
13514
+ ownedKeys: [
13515
+ "permissions",
13516
+ "outputStyle",
13517
+ "general",
13518
+ "tools",
13519
+ "mcp",
13520
+ "agents",
13521
+ "autoMode",
13522
+ "agentsMdExcludes"
13523
+ ]
13141
13524
  }
13142
13525
  }
13143
13526
  },
13527
+ ".trae/permission/global.json": {
13528
+ format: "json",
13529
+ invalidRootPolicy: "error",
13530
+ features: { permissions: {
13531
+ kind: "replace-owned-keys",
13532
+ ownedKeys: ["customProfiles", "resourceAuthorization"]
13533
+ } }
13534
+ },
13144
13535
  ".tabnine/agent/settings.json": {
13145
13536
  format: "json",
13146
13537
  invalidRootPolicy: "error",
@@ -13414,6 +13805,14 @@ const SHARED_CONFIG_OWNERSHIP = {
13414
13805
  ownedKeys: ["toolPermissions"]
13415
13806
  }
13416
13807
  }
13808
+ },
13809
+ [OPENCLAW_CONFIG_SHARED_FILE_KEY]: {
13810
+ format: "json5",
13811
+ invalidRootPolicy: "error",
13812
+ features: { mcp: {
13813
+ kind: "replace-owned-keys",
13814
+ ownedKeys: ["mcp"]
13815
+ } }
13417
13816
  }
13418
13817
  };
13419
13818
  /**
@@ -21956,6 +22355,190 @@ var KiroIdeCommand = class extends KiroCommand {
21956
22355
  }
21957
22356
  };
21958
22357
  //#endregion
22358
+ //#region src/constants/lettacode-paths.ts
22359
+ const LETTACODE_DIR = ".letta";
22360
+ const LETTACODE_SETTINGS_FILE_NAME = "settings.json";
22361
+ const LETTACODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
22362
+ const LETTACODE_IGNORE_FILE_NAME = ".lettaignore";
22363
+ const LETTACODE_AGENTS_DIR_PATH = join(LETTACODE_DIR, "agents");
22364
+ const LETTACODE_PROJECT_SKILLS_DIR_PATH = join(".agents", "skills");
22365
+ const LETTACODE_GLOBAL_SKILLS_DIR_PATH = join(LETTACODE_DIR, "skills");
22366
+ const LETTACODE_PROJECT_COMMANDS_DIR_PATH = ".commands";
22367
+ const LETTACODE_GLOBAL_COMMANDS_DIR_PATH = join(LETTACODE_DIR, "commands");
22368
+ //#endregion
22369
+ //#region src/features/commands/lettacode-command.ts
22370
+ /**
22371
+ * The only frontmatter keys Letta Code reads from a custom command
22372
+ * (`parseCommandFile` in `src/cli/commands/custom.ts`). Anything else — e.g.
22373
+ * `allowed-tools`, `model` or `disable-model-invocation` copied over from a
22374
+ * Claude Code command — is parsed and ignored upstream.
22375
+ */
22376
+ const LETTACODE_COMMAND_FRONTMATTER_KEYS = ["description", "argument-hint"];
22377
+ const LettacodeCommandFrontmatterSchema = z.looseObject({
22378
+ description: z.optional(z.string()),
22379
+ "argument-hint": z.optional(z.string())
22380
+ });
22381
+ function pickLettacodeFrontmatter({ section, relativeFilePath, logger }) {
22382
+ if (!isRecord$1(section)) return {};
22383
+ const picked = {};
22384
+ for (const [key, value] of Object.entries(section)) {
22385
+ if (LETTACODE_COMMAND_FRONTMATTER_KEYS.includes(key)) {
22386
+ picked[key] = value;
22387
+ continue;
22388
+ }
22389
+ warnWithFallback(logger, `Letta Code commands: dropping lettacode.${stripControlCharacters(key)} from ${quoteForLog(relativeFilePath)}: Letta Code only reads ${LETTACODE_COMMAND_FRONTMATTER_KEYS.join(" and ")}.`);
22390
+ }
22391
+ return picked;
22392
+ }
22393
+ /**
22394
+ * Letta Code custom slash commands: Markdown prompt files under `.commands/`
22395
+ * (project, at the working-directory root) and `~/.letta/commands/` (global),
22396
+ * discovered recursively. The file basename is the command id
22397
+ * (`git/commit.md` is invoked as `/commit`; `findCustomCommand` matches the
22398
+ * id only), so two files with the same basename collide and the first one
22399
+ * found wins; a project command wins over a global one with the same id. Frontmatter supports `description` (defaulting to the first body
22400
+ * line) and `argument-hint`; the body supports `$ARGUMENTS`, `$1`..`$9`,
22401
+ * `` !`command` `` and `@file`.
22402
+ *
22403
+ * `description` comes from the rulesync frontmatter; a `lettacode:` section
22404
+ * may set `argument-hint` (and override `description`). Other keys in that
22405
+ * section are dropped with a warning because Letta Code never reads them.
22406
+ *
22407
+ * @see https://docs.letta.com/platform/cli/slash-commands
22408
+ * @see https://github.com/letta-ai/letta-code/blob/main/src/cli/commands/custom.ts
22409
+ */
22410
+ var LettacodeCommand = class LettacodeCommand extends ToolCommand {
22411
+ frontmatter;
22412
+ body;
22413
+ constructor({ frontmatter, body, ...rest }) {
22414
+ if (rest.validate) {
22415
+ const result = LettacodeCommandFrontmatterSchema.safeParse(frontmatter);
22416
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
22417
+ }
22418
+ super({
22419
+ ...rest,
22420
+ fileContent: stringifyFrontmatter(body, frontmatter)
22421
+ });
22422
+ this.frontmatter = frontmatter;
22423
+ this.body = body;
22424
+ }
22425
+ static getSettablePaths({ global = false } = {}) {
22426
+ return { relativeDirPath: global ? LETTACODE_GLOBAL_COMMANDS_DIR_PATH : LETTACODE_PROJECT_COMMANDS_DIR_PATH };
22427
+ }
22428
+ getBody() {
22429
+ return this.body;
22430
+ }
22431
+ getFrontmatter() {
22432
+ return this.frontmatter;
22433
+ }
22434
+ toRulesyncCommand() {
22435
+ const { description, ...restFields } = this.frontmatter;
22436
+ const rulesyncFrontmatter = {
22437
+ targets: ["*"],
22438
+ description,
22439
+ ...Object.keys(restFields).length > 0 && { lettacode: restFields }
22440
+ };
22441
+ return new RulesyncCommand({
22442
+ outputRoot: ".",
22443
+ frontmatter: rulesyncFrontmatter,
22444
+ body: this.body,
22445
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
22446
+ relativeFilePath: this.relativeFilePath,
22447
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
22448
+ validate: true
22449
+ });
22450
+ }
22451
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
22452
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
22453
+ const lettacodeFields = pickLettacodeFrontmatter({
22454
+ section: rulesyncFrontmatter.lettacode,
22455
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
22456
+ logger
22457
+ });
22458
+ const frontmatter = {
22459
+ ...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
22460
+ ...lettacodeFields
22461
+ };
22462
+ const paths = this.getSettablePaths({ global });
22463
+ return new LettacodeCommand({
22464
+ outputRoot,
22465
+ frontmatter,
22466
+ body: rulesyncCommand.getBody(),
22467
+ relativeDirPath: paths.relativeDirPath,
22468
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
22469
+ validate,
22470
+ global
22471
+ });
22472
+ }
22473
+ validate() {
22474
+ if (!this.frontmatter) return {
22475
+ success: true,
22476
+ error: null
22477
+ };
22478
+ const result = LettacodeCommandFrontmatterSchema.safeParse(this.frontmatter);
22479
+ if (result.success) return {
22480
+ success: true,
22481
+ error: null
22482
+ };
22483
+ return {
22484
+ success: false,
22485
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
22486
+ };
22487
+ }
22488
+ /**
22489
+ * Warn when two targeted commands share a basename. Letta Code invokes a
22490
+ * command by its basename only, so `git/commit.md` and `release/commit.md`
22491
+ * are both `/commit` and only the first one Letta Code finds is reachable.
22492
+ * Both files are still written, so this warns instead of failing the run.
22493
+ */
22494
+ static validateRulesyncCommands({ rulesyncCommands, logger }) {
22495
+ const pathsById = /* @__PURE__ */ new Map();
22496
+ for (const command of rulesyncCommands) {
22497
+ if (!this.isTargetedByRulesyncCommand(command)) continue;
22498
+ const relativeFilePath = command.getRelativeFilePath();
22499
+ const id = basename(relativeFilePath, ".md");
22500
+ pathsById.set(id, [...pathsById.get(id) ?? [], relativeFilePath]);
22501
+ }
22502
+ for (const [id, paths] of pathsById) {
22503
+ if (paths.length < 2) continue;
22504
+ logger.warn(`Letta Code invokes commands by file basename, so ${paths.map((path) => quoteForLog(path)).join(", ")} are all /${stripControlCharacters(id)} and only one of them is reachable. Rename them to make each invocable.`);
22505
+ }
22506
+ }
22507
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
22508
+ return this.isTargetedByRulesyncCommandDefault({
22509
+ rulesyncCommand,
22510
+ toolTarget: "lettacode"
22511
+ });
22512
+ }
22513
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
22514
+ const paths = this.getSettablePaths({ global });
22515
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
22516
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
22517
+ const result = LettacodeCommandFrontmatterSchema.safeParse(frontmatter);
22518
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
22519
+ return new LettacodeCommand({
22520
+ outputRoot,
22521
+ relativeDirPath: paths.relativeDirPath,
22522
+ relativeFilePath,
22523
+ frontmatter: result.data,
22524
+ body: body.trim(),
22525
+ validate,
22526
+ global
22527
+ });
22528
+ }
22529
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
22530
+ return new LettacodeCommand({
22531
+ outputRoot,
22532
+ relativeDirPath,
22533
+ relativeFilePath,
22534
+ frontmatter: {},
22535
+ body: "",
22536
+ validate: false,
22537
+ global
22538
+ });
22539
+ }
22540
+ };
22541
+ //#endregion
21959
22542
  //#region src/constants/mimocode-paths.ts
21960
22543
  /**
21961
22544
  * MiMo Code (Xiaomi's OpenCode fork) reads `.mimocode/` in the project and
@@ -21973,7 +22556,8 @@ const MIMOCODE_LAYOUT = {
21973
22556
  configDir: MIMOCODE_DIR,
21974
22557
  jsonFileName: "mimocode.json",
21975
22558
  jsoncFileName: "mimocode.jsonc",
21976
- readsV2ConfigSpellings: false
22559
+ readsV2ConfigSpellings: false,
22560
+ readsDotDirConfig: false
21977
22561
  };
21978
22562
  //#endregion
21979
22563
  //#region src/constants/opencode-paths.ts
@@ -22000,7 +22584,8 @@ const OPENCODE_LAYOUT = {
22000
22584
  configDir: ".",
22001
22585
  jsonFileName: OPENCODE_JSON_FILE_NAME,
22002
22586
  jsoncFileName: OPENCODE_JSONC_FILE_NAME,
22003
- readsV2ConfigSpellings: true
22587
+ readsV2ConfigSpellings: true,
22588
+ readsDotDirConfig: true
22004
22589
  };
22005
22590
  //#endregion
22006
22591
  //#region src/features/opencode-config.ts
@@ -22034,6 +22619,107 @@ async function readOpencodeConfig({ outputRoot, global = false, layout = OPENCOD
22034
22619
  });
22035
22620
  }
22036
22621
  /**
22622
+ * Reads the project-scope config files OpenCode merges over the root
22623
+ * `opencode.json(c)`: `.opencode/opencode.json`, then `.opencode/opencode.jsonc`,
22624
+ * in the order OpenCode loads them (a later file wins). Returns an empty list
22625
+ * in global mode and for layouts without this location (MiMo Code).
22626
+ *
22627
+ * @see https://opencode.ai/docs/config/#precedence-order
22628
+ * @see https://github.com/anomalyco/opencode/blob/v1.19.3/packages/opencode/src/config/config.ts
22629
+ */
22630
+ async function readOpencodeDotDirConfigLayers({ outputRoot, global = false, layout = OPENCODE_LAYOUT }) {
22631
+ if (global || !layout.readsDotDirConfig) return [];
22632
+ const configDir = join(outputRoot, layout.dir);
22633
+ const layers = [];
22634
+ for (const fileName of [layout.jsonFileName, layout.jsoncFileName]) {
22635
+ const fileContent = await readFileContentOrNull(join(configDir, fileName));
22636
+ if (fileContent === null) continue;
22637
+ layers.push({
22638
+ relativeFilePath: join(layout.dir, fileName),
22639
+ configDir,
22640
+ config: parseSharedConfig({
22641
+ format: "jsonc",
22642
+ fileContent
22643
+ })
22644
+ });
22645
+ }
22646
+ return layers;
22647
+ }
22648
+ /**
22649
+ * Reads every OpenCode config file that applies at this scope, lowest
22650
+ * precedence first: the root config (see {@link readOpencodeConfig}), then the
22651
+ * `.opencode/` files (see {@link readOpencodeDotDirConfigLayers}).
22652
+ */
22653
+ async function readOpencodeConfigLayers({ outputRoot, global = false, layout = OPENCODE_LAYOUT }) {
22654
+ return [{
22655
+ configDir: getOpencodeConfigDir({
22656
+ outputRoot,
22657
+ global,
22658
+ layout
22659
+ }),
22660
+ config: await readOpencodeConfig({
22661
+ outputRoot,
22662
+ global,
22663
+ layout
22664
+ })
22665
+ }, ...await readOpencodeDotDirConfigLayers({
22666
+ outputRoot,
22667
+ global,
22668
+ layout
22669
+ })];
22670
+ }
22671
+ /**
22672
+ * Deep-merges `source` over `target` the way OpenCode merges its config files
22673
+ * (remeda's `mergeDeep`): plain objects merge key by key, and any other value —
22674
+ * arrays included — replaces the target's.
22675
+ */
22676
+ function mergeOpencodeConfigValues(target, source) {
22677
+ if (!isRecord$1(target) || !isRecord$1(source)) return source;
22678
+ const merged = {};
22679
+ for (const [key, value] of Object.entries(target)) setOwnEntry(merged, key, value);
22680
+ for (const [key, value] of Object.entries(source)) setOwnEntry(merged, key, Object.hasOwn(target, key) ? mergeOpencodeConfigValues(target[key], value) : value);
22681
+ return merged;
22682
+ }
22683
+ /**
22684
+ * Merges per-file entry maps (the `command` or `agent` entries of each config
22685
+ * layer, lowest precedence first) into one, as OpenCode does. Returns `null`
22686
+ * when no layer has entries.
22687
+ */
22688
+ function mergeOpencodeEntryLayers(layers) {
22689
+ let merged = null;
22690
+ for (const entries of layers) {
22691
+ if (entries === null) continue;
22692
+ const next = mergeOpencodeConfigValues(merged ?? {}, entries);
22693
+ merged = isRecord$1(next) ? next : merged;
22694
+ }
22695
+ return merged;
22696
+ }
22697
+ /**
22698
+ * Warns when a `.opencode/opencode.json(c)` file sets entries that the
22699
+ * generated root config also sets. OpenCode merges those files over the root
22700
+ * config, so their entries win and the generated ones never take effect.
22701
+ *
22702
+ * `generated` maps each top-level key rulesync writes (`mcp`, `permission`) to
22703
+ * its generated value. `overlays` are the `.opencode/` layers, already lowered
22704
+ * by the caller where needed.
22705
+ */
22706
+ function warnOpencodeDotDirOverrides({ generated, overlays, rootFileName, logger }) {
22707
+ if (!logger) return;
22708
+ for (const overlay of overlays) {
22709
+ const overridden = [];
22710
+ for (const [key, value] of Object.entries(generated)) {
22711
+ if (value === void 0 || !Object.hasOwn(overlay.config, key)) continue;
22712
+ const overlayValue = overlay.config[key];
22713
+ if (!isRecord$1(value) || !isRecord$1(overlayValue)) {
22714
+ overridden.push(`"${key}"`);
22715
+ continue;
22716
+ }
22717
+ for (const name of Object.keys(value)) if (Object.hasOwn(overlayValue, name)) overridden.push(`"${key}" > ${quoteValueForWarning(name)}`);
22718
+ }
22719
+ if (overridden.length > 0) logger.warn(`OpenCode merges "${toPosixPath(overlay.relativeFilePath)}" over the generated "${rootFileName}", so its values for these generated entries take precedence: ${overridden.join(", ")}.`);
22720
+ }
22721
+ }
22722
+ /**
22037
22723
  * Narrows an unknown value to a plain record of entries keyed by name, as used
22038
22724
  * for OpenCode's `command` / `agent` config sections. Returns `null` when the
22039
22725
  * value is not a usable object.
@@ -22476,8 +23162,9 @@ var OpenCodeCommand = class extends ToolCommand {
22476
23162
  });
22477
23163
  }
22478
23164
  /**
22479
- * Imports commands defined inline in `opencode.json` / `opencode.jsonc` under
22480
- * the top-level `command` key — or its V2 spelling `commands`, see
23165
+ * Imports commands defined inline in `opencode.json` / `opencode.jsonc` (and,
23166
+ * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
23167
+ * them) under the top-level `command` key — or its V2 spelling `commands`, see
22481
23168
  * `getOpencodeCommandEntries` — (in addition to the Markdown files under
22482
23169
  * `.opencode/commands/`). Each entry's `template` becomes the command body,
22483
23170
  * while `description` / `agent` / `model` / `variant` / `subtask` map to the
@@ -22490,14 +23177,14 @@ var OpenCodeCommand = class extends ToolCommand {
22490
23177
  * @see https://opencode.ai/docs/commands/#json
22491
23178
  */
22492
23179
  static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false } = {}) {
22493
- const commandEntries = getOpencodeCommandEntries({
22494
- config: await readOpencodeConfig({
22495
- outputRoot,
22496
- global,
22497
- layout: this.layout
22498
- }),
23180
+ const commandEntries = mergeOpencodeEntryLayers((await readOpencodeConfigLayers({
23181
+ outputRoot,
23182
+ global,
22499
23183
  layout: this.layout
22500
- });
23184
+ })).map(({ config }) => getOpencodeCommandEntries({
23185
+ config,
23186
+ layout: this.layout
23187
+ })));
22501
23188
  if (!commandEntries) return [];
22502
23189
  const paths = this.getSettablePaths({ global });
22503
23190
  const commands = [];
@@ -23883,6 +24570,7 @@ var TaktCommand = class TaktCommand extends ToolCommand {
23883
24570
  * @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
23884
24571
  * @see https://docs.trae.ai/ide/slash-commands?_lang=en
23885
24572
  * @see https://docs.trae.ai/ide/ignore-files?_lang=en
24573
+ * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
23886
24574
  */
23887
24575
  const TRAE_DIR = ".trae";
23888
24576
  /** Project MCP servers (`mcpServers` object), project scope only. */
@@ -23896,6 +24584,13 @@ const TRAE_SKILLS_DIR_PATH = join(TRAE_DIR, "skills");
23896
24584
  const TRAE_COMMANDS_DIR_PATH = join(TRAE_DIR, "commands");
23897
24585
  /** Indexing ignore list (gitignore syntax), `.trae/.ignore`, project scope only. */
23898
24586
  const TRAE_IGNORE_FILE_NAME = ".ignore";
24587
+ /**
24588
+ * Global permission policy: `~/.trae/permission/global.json`, user scope only. It
24589
+ * also holds the custom permission mode (`customProfiles`), so it is edited in
24590
+ * place rather than owned outright.
24591
+ */
24592
+ const TRAE_PERMISSION_DIR_PATH = join(TRAE_DIR, "permission");
24593
+ const TRAE_PERMISSION_FILE_NAME = "global.json";
23899
24594
  //#endregion
23900
24595
  //#region src/features/commands/trae-command.ts
23901
24596
  const TraeCommandFrontmatterSchema = z.looseObject({
@@ -24661,6 +25356,17 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
24661
25356
  supportsSubdirectory: false
24662
25357
  }
24663
25358
  }],
25359
+ ["lettacode", {
25360
+ class: LettacodeCommand,
25361
+ meta: {
25362
+ extension: "md",
25363
+ supportsProject: true,
25364
+ supportsGlobal: true,
25365
+ isSimulated: false,
25366
+ supportsSubdirectory: true,
25367
+ committedOutput: true
25368
+ }
25369
+ }],
24664
25370
  ["mimocode", {
24665
25371
  class: MimocodeCommand,
24666
25372
  meta: {
@@ -24970,12 +25676,25 @@ var CommandsProcessor = class extends FeatureProcessor {
24970
25676
  return rulesyncCommands;
24971
25677
  }
24972
25678
  /**
25679
+ * Whether {@link loadToolFiles} must not scan the tool's output tree at all.
25680
+ * `skipToolFileScan` targets never scan it; a `committedOutput` target is not
25681
+ * swept at project scope, because nothing on disk tells a hand-written
25682
+ * command in such a committed root apart from one rulesync generated.
25683
+ */
25684
+ skipsToolFileScan({ factory, forDeletion }) {
25685
+ if (factory.meta.skipToolFileScan) return true;
25686
+ return forDeletion && factory.meta.committedOutput === true && !this.global;
25687
+ }
25688
+ /**
24973
25689
  * Implementation of abstract method from FeatureProcessor
24974
25690
  * Load tool-specific command configurations and parse them into ToolCommand instances
24975
25691
  */
24976
25692
  async loadToolFiles({ forDeletion = false } = {}) {
24977
25693
  const factory = this.getFactory(this.toolTarget);
24978
- if (factory.meta.skipToolFileScan) return [];
25694
+ if (this.skipsToolFileScan({
25695
+ factory,
25696
+ forDeletion
25697
+ })) return [];
24979
25698
  const paths = factory.class.getSettablePaths({ global: this.global });
24980
25699
  const outputRootFull = join(this.outputRoot, paths.relativeDirPath);
24981
25700
  const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ? `**/*.${factory.meta.extension}` : `*.${factory.meta.extension}`, {
@@ -26692,13 +27411,48 @@ function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, w
26692
27411
  })
26693
27412
  };
26694
27413
  }
27414
+ /**
27415
+ * Why a definition must not be emitted for this tool, or `undefined` when it
27416
+ * can be: it carries a restricting field the tool refuses to honour (see
27417
+ * `skippedWhenFields`).
27418
+ */
27419
+ function describeExportSkipReason({ def, hookType, eventName, converterConfig }) {
27420
+ const field = converterConfig.skippedWhenFields?.find((name) => isNonEmptyString(def[name]));
27421
+ if (field === void 0) return;
27422
+ return `Skipping a "${eventName}" ${hookType} hook (${quoteValueForWarning(def.command ?? def.url)}): this tool does not support its "${field}" condition (${quoteValueForWarning(def[field])}), and without it the hook would run on every call its matcher selects.`;
27423
+ }
27424
+ /**
27425
+ * Whether a definition can be emitted for this tool: its hook type is one the
27426
+ * tool supports, and it carries no restricting field the tool refuses to
27427
+ * honour (warned about here).
27428
+ */
27429
+ function isEmittableDefinition({ def, hookType, eventName, converterConfig, warn }) {
27430
+ if (!isSupportedHookType({
27431
+ type: hookType,
27432
+ converterConfig
27433
+ })) return false;
27434
+ const skipReason = describeExportSkipReason({
27435
+ def,
27436
+ hookType,
27437
+ eventName,
27438
+ converterConfig
27439
+ });
27440
+ if (skipReason !== void 0) {
27441
+ warn?.(skipReason);
27442
+ return false;
27443
+ }
27444
+ return true;
27445
+ }
26695
27446
  function buildToolHooks({ defs, eventName, converterConfig, warn }) {
26696
27447
  const hooks = [];
26697
27448
  for (const def of defs) {
26698
27449
  const hookType = def.type ?? "command";
26699
- if (!isSupportedHookType({
26700
- type: hookType,
26701
- converterConfig
27450
+ if (!isEmittableDefinition({
27451
+ def,
27452
+ hookType,
27453
+ eventName,
27454
+ converterConfig,
27455
+ warn
26702
27456
  })) continue;
26703
27457
  const command = applyCommandPrefix({
26704
27458
  def,
@@ -27270,7 +28024,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
27270
28024
  warn
27271
28025
  }));
27272
28026
  }
27273
- if (defs.length > 0) canonical[eventName] = defs;
28027
+ if (defs.length > 0) {
28028
+ const existing = Object.hasOwn(canonical, eventName) ? canonical[eventName] : void 0;
28029
+ canonical[eventName] = existing ? [...existing, ...defs] : defs;
28030
+ }
27274
28031
  }
27275
28032
  return canonical;
27276
28033
  }
@@ -31980,6 +32737,16 @@ const GROKCLI_CONVERTER_CONFIG = {
31980
32737
  tool: "env",
31981
32738
  commandOnly: true
31982
32739
  }],
32740
+ arrayPassthroughFields: [{
32741
+ canonical: "args",
32742
+ tool: "args",
32743
+ commandOnly: true
32744
+ }],
32745
+ skippedWhenFields: ["if"],
32746
+ stringPassthroughFields: [{
32747
+ canonical: "if",
32748
+ tool: "if"
32749
+ }],
31983
32750
  noMatcherEvents: /* @__PURE__ */ new Set(["stop", "beforeSubmitPrompt"])
31984
32751
  };
31985
32752
  /**
@@ -33476,15 +34243,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
33476
34243
  }
33477
34244
  };
33478
34245
  //#endregion
33479
- //#region src/constants/lettacode-paths.ts
33480
- const LETTACODE_DIR = ".letta";
33481
- const LETTACODE_SETTINGS_FILE_NAME = "settings.json";
33482
- const LETTACODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
33483
- const LETTACODE_IGNORE_FILE_NAME = ".lettaignore";
33484
- const LETTACODE_AGENTS_DIR_PATH = join(LETTACODE_DIR, "agents");
33485
- const LETTACODE_PROJECT_SKILLS_DIR_PATH = join(".agents", "skills");
33486
- const LETTACODE_GLOBAL_SKILLS_DIR_PATH = join(LETTACODE_DIR, "skills");
33487
- //#endregion
33488
34246
  //#region src/features/hooks/lettacode-hooks.ts
33489
34247
  const LETTACODE_CONVERTER_CONFIG = {
33490
34248
  supportedEvents: LETTACODE_HOOK_EVENTS,
@@ -43173,8 +43931,10 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
43173
43931
  /**
43174
43932
  * Crush-only MCP fields with no canonical counterpart, authored under a
43175
43933
  * `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
43176
- * name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers and
43177
- * `sessionless` marks a server that issues no `Mcp-Session-Id`.
43934
+ * name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers,
43935
+ * `sessionless` marks a server that issues no `Mcp-Session-Id`; and
43936
+ * `channel_enabled` / `channel_reply` (v0.97.0) turn a server that implements
43937
+ * Claude Channels into a channel and route replies back through its tools.
43178
43938
  * `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
43179
43939
  * never reaches another tool's config; `fromRulesyncMcp` re-merges them from
43180
43940
  * the raw source JSON, honouring the raw Crush spelling as a fallback for an
@@ -43213,6 +43973,18 @@ const CRUSH_ONLY_KEYS = [
43213
43973
  crush: "sessionless",
43214
43974
  accepts: (v) => typeof v === "boolean",
43215
43975
  rawFallback: true
43976
+ },
43977
+ {
43978
+ canonical: "crushChannelEnabled",
43979
+ crush: "channel_enabled",
43980
+ accepts: (v) => typeof v === "boolean",
43981
+ rawFallback: true
43982
+ },
43983
+ {
43984
+ canonical: "crushChannelReply",
43985
+ crush: "channel_reply",
43986
+ accepts: (v) => CrushChannelReplySchema.safeParse(v).success,
43987
+ rawFallback: true
43216
43988
  }
43217
43989
  ];
43218
43990
  /**
@@ -43220,12 +43992,16 @@ const CRUSH_ONLY_KEYS = [
43220
43992
  * canonical name, so `convertToCrushFormat` sees them after the shared map
43221
43993
  * stripped them.
43222
43994
  */
43223
- function readCrushOnlyKeys(rawServer) {
43995
+ function readCrushOnlyKeys({ serverName, rawServer, logger }) {
43224
43996
  const result = {};
43225
43997
  if (!isRecord$1(rawServer)) return result;
43226
43998
  for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
43227
43999
  const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
43228
44000
  if (value !== void 0) result[canonical] = value;
44001
+ else if (canonical === "crushChannelReply") {
44002
+ const rejected = [canonical, crush].find((key) => rawServer[key] !== void 0);
44003
+ if (rejected !== void 0) logger?.warn(`Crush MCP server "${serverName}": ignoring "${rejected}" because it does not match Crush's channel_reply shape ({ user?, group?: { tool, target_param?, target_meta? }, message_param?, suppress_tools? }).`);
44004
+ }
43229
44005
  }
43230
44006
  return result;
43231
44007
  }
@@ -43350,8 +44126,8 @@ function convertToCrushFormat(mcpServers, logger) {
43350
44126
  * servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
43351
44127
  * server with a `command` is stdio already); `http` and `sse` are kept as the
43352
44128
  * canonical transport names; `disabled_tools`/`enabled_tools` become the
43353
- * canonical `disabledTools`/`enabledTools`; the Crush-only OAuth and
43354
- * `sessionless` fields are lifted into their `crush*` authoring keys (a value
44129
+ * canonical `disabledTools`/`enabledTools`; the Crush-only OAuth,
44130
+ * `sessionless` and channel fields are lifted into their `crush*` authoring keys (a value
43355
44131
  * of the wrong type is dropped, as Crush itself would refuse it), and any
43356
44132
  * other key is kept verbatim so an import preserves what a hand-authored
43357
44133
  * entry declared.
@@ -43442,7 +44218,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
43442
44218
  });
43443
44219
  const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
43444
44220
  ...serverConfig,
43445
- ...readCrushOnlyKeys(rulesyncMcp.getRawMcpServer(serverName))
44221
+ ...readCrushOnlyKeys({
44222
+ serverName,
44223
+ rawServer: rulesyncMcp.getRawMcpServer(serverName),
44224
+ logger
44225
+ })
43446
44226
  }])), logger);
43447
44227
  return new CrushMcp({
43448
44228
  outputRoot,
@@ -44035,12 +44815,19 @@ var DevinPluginMcp = class DevinPluginMcp extends DevinMcp {
44035
44815
  * only the default location. Project-scoped assets live under a `.dsh/`
44036
44816
  * directory at the project root.
44037
44817
  *
44038
- * Rules: `dsh-agent-instructions` loads the user-global `$DSH_HOME/AGENTS.md`
44039
- * (root file only — the user-global file has no `.local.md` overlay), then the
44040
- * project chain of `AGENTS.md` / `CLAUDE.md` files from the project root (the
44041
- * nearest ancestor holding `.git`) down to the session cwd, broad to specific.
44042
- * Rulesync emits `AGENTS.md` only; `CLAUDE.md` is the `claudecode` target's
44043
- * file, and a `CLAUDE.md` duplicating its sibling `AGENTS.md` renders once.
44818
+ * Rules: `dsh-agent-instructions` loads the user-global chain —
44819
+ * `$DSH_HOME/AGENTS.md`, then `<agentsHome>/AGENTS.md` (`$DSH_AGENTS_HOME`,
44820
+ * default `~/.agents`, since dsh-v0.2.1-alpha.2) — then the project chain of
44821
+ * `AGENTS.md` / `CLAUDE.md` files plus their `AGENTS.local.md` /
44822
+ * `CLAUDE.local.md` overlays from the project root (the nearest ancestor
44823
+ * holding `.git`) down to the session cwd, broad to specific. Neither
44824
+ * user-global root has a `.local.md` overlay. The two user-global roots share
44825
+ * one dedupe group, so the shared-root file collapses behind the harness-home
44826
+ * file only when both match after trimming; otherwise both load. Rulesync
44827
+ * writes only `$DSH_HOME/AGENTS.md` globally (never the shared
44828
+ * `~/.agents/AGENTS.md`), and `AGENTS.md` / `AGENTS.local.md` in the project;
44829
+ * `CLAUDE.md` is the `claudecode` target's file, and a `CLAUDE.md` duplicating
44830
+ * its sibling `AGENTS.md` renders once.
44044
44831
  *
44045
44832
  * Skills: `dsh-skill-filesystem` scans `<projectRoot>/.dsh/skills` (rank 100),
44046
44833
  * `<projectRoot>/.agents/skills` (200), `<dshHome>/skills` (400) and
@@ -46384,6 +47171,20 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
46384
47171
  if (transport === "http" || transport === "streamable-http") return "http";
46385
47172
  return hasCommand ? "stdio" : "http";
46386
47173
  }
47174
+ /**
47175
+ * The only `executor` value Kimi Code accepts on a stdio server. 2.2.0 removed
47176
+ * the never-implemented `kaos` option and narrowed the schema to
47177
+ * `z.enum(['local'])`, so any other value makes Kimi fail validation when it
47178
+ * loads `mcp.json`.
47179
+ * @see https://github.com/MoonshotAI/kimi-code/pull/4184
47180
+ */
47181
+ const KIMI_CODE_MCP_EXECUTOR = "local";
47182
+ /** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
47183
+ function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
47184
+ if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
47185
+ warnWithFallback(logger, `Kimi Code MCP: dropping executor ${JSON.stringify(server.executor)} from "${name}" because Kimi Code only accepts "${KIMI_CODE_MCP_EXECUTOR}".`);
47186
+ delete server.executor;
47187
+ }
46387
47188
  function toKimiCodeServer({ name, server, logger }) {
46388
47189
  const transport = server.transport ?? server.type;
46389
47190
  if (transport === "ws") {
@@ -46422,6 +47223,11 @@ function toKimiCodeServer({ name, server, logger }) {
46422
47223
  "enabledTools",
46423
47224
  "disabledTools"
46424
47225
  ]) if (server[field] !== void 0) converted[field] = server[field];
47226
+ dropUnsupportedKimiCodeExecutor({
47227
+ name,
47228
+ server: converted,
47229
+ logger
47230
+ });
46425
47231
  if (server.disabled === true) converted.enabled = false;
46426
47232
  return converted;
46427
47233
  }
@@ -46437,9 +47243,14 @@ function toKimiCodeServers({ servers, logger }) {
46437
47243
  }
46438
47244
  return result;
46439
47245
  }
46440
- function fromKimiCodeServers(servers) {
47246
+ function fromKimiCodeServers({ servers, logger }) {
46441
47247
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46442
47248
  const { transport, enabled, ...rest } = server;
47249
+ dropUnsupportedKimiCodeExecutor({
47250
+ name,
47251
+ server: rest,
47252
+ logger
47253
+ });
46443
47254
  const type = transport === "stdio" || transport === "sse" || transport === "http" ? transport : void 0;
46444
47255
  return [name, {
46445
47256
  ...rest,
@@ -46554,9 +47365,11 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46554
47365
  * a different file that only the async `fromFile` can read.
46555
47366
  */
46556
47367
  configDefaults;
46557
- constructor(params) {
47368
+ logger;
47369
+ constructor({ configDefaults, logger, ...params }) {
46558
47370
  super(params);
46559
- this.configDefaults = params.configDefaults ?? {};
47371
+ this.configDefaults = configDefaults ?? {};
47372
+ this.logger = logger;
46560
47373
  try {
46561
47374
  this.json = this.fileContent ? JSON.parse(this.fileContent) : {};
46562
47375
  } catch (error) {
@@ -46572,7 +47385,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46572
47385
  relativeFilePath: KIMI_CODE_MCP_FILE_NAME
46573
47386
  };
46574
47387
  }
46575
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
47388
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
46576
47389
  const paths = this.getSettablePaths({ global });
46577
47390
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
46578
47391
  return new KimiCodeMcp({
@@ -46582,7 +47395,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46582
47395
  fileContent,
46583
47396
  validate,
46584
47397
  global,
46585
- configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {}
47398
+ configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {},
47399
+ logger
46586
47400
  });
46587
47401
  }
46588
47402
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
@@ -46656,7 +47470,10 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
46656
47470
  relativeFilePath: RULESYNC_MCP_FILE_NAME,
46657
47471
  fileContent: JSON.stringify({
46658
47472
  ...this.json,
46659
- mcpServers: fromKimiCodeServers(isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {}),
47473
+ mcpServers: fromKimiCodeServers({
47474
+ servers: isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {},
47475
+ logger: this.logger
47476
+ }),
46660
47477
  ...Object.keys(this.configDefaults).length > 0 && { "kimi-code": this.configDefaults }
46661
47478
  }, null, 2)
46662
47479
  });
@@ -47097,7 +47914,13 @@ var OpencodeMcp = class extends ToolMcp {
47097
47914
  fileContent = await readFileContentOrNull(jsonPath);
47098
47915
  if (fileContent) relativeFilePath = this.layout.jsonFileName;
47099
47916
  }
47100
- const json = parse$1(fileContent ?? "{\"mcp\":{}}");
47917
+ const fileContentToUse = fileContent ?? "{\"mcp\":{}}";
47918
+ const overlays = await readOpencodeDotDirConfigLayers({
47919
+ outputRoot,
47920
+ global,
47921
+ layout: this.layout
47922
+ });
47923
+ const json = overlays.length === 0 ? parse$1(fileContentToUse) : [parse$1(fileContentToUse), ...overlays.map(({ config }) => config)].map((layer) => this.lowerConfig(layer, logger)).reduce(mergeOpencodeConfigValues);
47101
47924
  const newJson = {
47102
47925
  ...json,
47103
47926
  mcp: json.mcp ?? {}
@@ -47134,6 +47957,27 @@ var OpencodeMcp = class extends ToolMcp {
47134
47957
  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.`);
47135
47958
  }
47136
47959
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
47960
+ const generatedMcp = undefinedWhenEmpty(convertedMcp);
47961
+ const generatedTools = Object.keys(mcpTools).length > 0 ? mcpTools : void 0;
47962
+ warnOpencodeDotDirOverrides({
47963
+ generated: {
47964
+ mcp: generatedMcp,
47965
+ tools: generatedTools
47966
+ },
47967
+ overlays: (await readOpencodeDotDirConfigLayers({
47968
+ outputRoot,
47969
+ global,
47970
+ layout: this.layout
47971
+ })).map((overlay) => {
47972
+ const lowered = this.lowerConfig(overlay.config);
47973
+ return {
47974
+ ...overlay,
47975
+ config: isRecord$1(lowered) ? lowered : {}
47976
+ };
47977
+ }),
47978
+ rootFileName: relativeFilePath,
47979
+ logger
47980
+ });
47137
47981
  return new this({
47138
47982
  outputRoot,
47139
47983
  relativeDirPath: basePaths.relativeDirPath,
@@ -47143,8 +47987,8 @@ var OpencodeMcp = class extends ToolMcp {
47143
47987
  feature: "mcp",
47144
47988
  existingContent: fileContent ?? "",
47145
47989
  patch: {
47146
- mcp: undefinedWhenEmpty(convertedMcp),
47147
- tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
47990
+ mcp: generatedMcp,
47991
+ tools: generatedTools
47148
47992
  },
47149
47993
  filePath: join(jsonDir, relativeFilePath),
47150
47994
  logger
@@ -47790,6 +48634,358 @@ var OmpMcp = class OmpMcp extends ToolMcp {
47790
48634
  }
47791
48635
  };
47792
48636
  //#endregion
48637
+ //#region src/constants/openclaw-paths.ts
48638
+ const OPENCLAW_WORKSPACE_DIR_PATH = join(".openclaw", "workspace");
48639
+ const OPENCLAW_RULE_FILE_NAME = "AGENTS.md";
48640
+ const OPENCLAW_CONFIG_DIR_PATH = ".openclaw";
48641
+ const OPENCLAW_CONFIG_FILE_NAME = "openclaw.json";
48642
+ const OPENCLAW_MCP_SERVERS_KEY = "servers";
48643
+ //#endregion
48644
+ //#region src/features/mcp/openclaw-mcp.ts
48645
+ const OPENCLAW_GLOBAL_ONLY_MESSAGE = "OpenClaw MCP is global-only; use --global to sync ~/.openclaw/openclaw.json";
48646
+ /**
48647
+ * Single spelling of the openclaw.json codec/policy, matching its
48648
+ * `SHARED_CONFIG_OWNERSHIP` declaration: the file is JSON5, and a file whose
48649
+ * root is not an object is refused rather than replaced, since it also holds
48650
+ * every other Gateway setting.
48651
+ */
48652
+ function parseOpenclawConfig(fileContent, filePath) {
48653
+ return parseSharedConfig({
48654
+ format: "json5",
48655
+ fileContent,
48656
+ filePath,
48657
+ invalidRootPolicy: "error"
48658
+ });
48659
+ }
48660
+ const OPENCLAW_OAUTH_STRING_KEYS = [
48661
+ "authProfileId",
48662
+ "scope",
48663
+ "redirectUrl",
48664
+ "clientMetadataUrl"
48665
+ ];
48666
+ const OPENCLAW_OAUTH_IDENTITIES = /* @__PURE__ */ new Set(["shared", "per-requester"]);
48667
+ /**
48668
+ * Copies the `oauth` keys OpenClaw accepts. A known key with a value its
48669
+ * schema rejects (an `identity` other than `shared` / `per-requester`, a blank
48670
+ * string) is dropped with a warning, since the Gateway would refuse to start
48671
+ * on it; keys OpenClaw does not know are left behind silently, as they belong
48672
+ * to other tools' `oauth` blocks.
48673
+ */
48674
+ function copyOpenclawOauth({ source, serverName, logger }) {
48675
+ const oauth = {};
48676
+ const dropped = [];
48677
+ if (source.identity !== void 0) {
48678
+ if (typeof source.identity === "string" && OPENCLAW_OAUTH_IDENTITIES.has(source.identity)) oauth.identity = source.identity;
48679
+ else dropped.push("identity");
48680
+ }
48681
+ for (const key of OPENCLAW_OAUTH_STRING_KEYS) {
48682
+ const value = source[key];
48683
+ if (value === void 0) continue;
48684
+ if (typeof value === "string" && value.trim() !== "") oauth[key] = value;
48685
+ else dropped.push(key);
48686
+ }
48687
+ if (dropped.length > 0 && serverName !== void 0) logger?.warn(`OpenClaw MCP: dropping invalid oauth ${dropped.map((key) => `"${key}"`).join(", ")} of ${quoteValueForWarning(serverName)}; OpenClaw's config validation would reject it.`);
48688
+ return Object.keys(oauth).length > 0 ? oauth : void 0;
48689
+ }
48690
+ const OPENCLAW_CODEX_APPROVAL_MODES = /* @__PURE__ */ new Set([
48691
+ "auto",
48692
+ "prompt",
48693
+ "approve"
48694
+ ]);
48695
+ /**
48696
+ * Copies the OpenClaw-only per-server fields that have no canonical
48697
+ * counterpart — `auth`, `oauth`, the TLS controls, the timeouts in
48698
+ * milliseconds, `supportsParallelToolCalls` and the `codex` projection — from
48699
+ * `source` to `target`, in both directions. OpenClaw validates its config
48700
+ * strictly (`oauth`, `toolFilter` and `codex` are strict objects) and refuses
48701
+ * to start the Gateway on a value it rejects, so only the documented keys with
48702
+ * the documented value types are copied; a same-named key another tool reads
48703
+ * differently (Hermes' snake_case `oauth` keys, say) is left behind.
48704
+ *
48705
+ * @see https://github.com/openclaw/openclaw/blob/v2026.10.4/src/config/zod-schema.mcp-server.ts
48706
+ */
48707
+ function copyOpenclawOnlyFields(source, target, { serverName, logger } = {}) {
48708
+ let copied = false;
48709
+ if (source.auth === "oauth") {
48710
+ target.auth = "oauth";
48711
+ copied = true;
48712
+ }
48713
+ if (isRecord$1(source.oauth)) {
48714
+ const oauth = copyOpenclawOauth({
48715
+ source: source.oauth,
48716
+ serverName,
48717
+ logger
48718
+ });
48719
+ if (oauth) {
48720
+ target.oauth = oauth;
48721
+ copied = true;
48722
+ }
48723
+ }
48724
+ for (const key of ["clientCert", "clientKey"]) if (typeof source[key] === "string") {
48725
+ target[key] = source[key];
48726
+ copied = true;
48727
+ }
48728
+ for (const key of ["sslVerify", "supportsParallelToolCalls"]) if (typeof source[key] === "boolean") {
48729
+ target[key] = source[key];
48730
+ copied = true;
48731
+ }
48732
+ for (const key of ["requestTimeoutMs", "connectionTimeoutMs"]) {
48733
+ const value = source[key];
48734
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
48735
+ target[key] = value;
48736
+ copied = true;
48737
+ }
48738
+ }
48739
+ if (isRecord$1(source.codex)) {
48740
+ const codex = {};
48741
+ if (isStringArray$1(source.codex.agents) && source.codex.agents.length > 0) codex.agents = [...source.codex.agents];
48742
+ const mode = source.codex.defaultToolsApprovalMode;
48743
+ if (typeof mode === "string" && OPENCLAW_CODEX_APPROVAL_MODES.has(mode)) codex.defaultToolsApprovalMode = mode;
48744
+ if (Object.keys(codex).length > 0) {
48745
+ target.codex = codex;
48746
+ copied = true;
48747
+ }
48748
+ }
48749
+ return copied;
48750
+ }
48751
+ /** A string map without pollution keys, or `undefined` when nothing is left. */
48752
+ function copyStringMap(value) {
48753
+ if (!isPlainObject$1(value)) return void 0;
48754
+ const result = {};
48755
+ for (const [key, entry] of Object.entries(value)) {
48756
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
48757
+ if (typeof entry === "string") result[key] = entry;
48758
+ else if (typeof entry === "number" || typeof entry === "boolean") result[key] = String(entry);
48759
+ }
48760
+ return Object.keys(result).length > 0 ? result : void 0;
48761
+ }
48762
+ /** A non-empty `toolFilter` list, trimmed of blank entries OpenClaw rejects. */
48763
+ function toolFilterList(value) {
48764
+ if (!isStringArray$1(value)) return void 0;
48765
+ const list = value.map((entry) => entry.trim()).filter((entry) => entry !== "");
48766
+ return list.length > 0 ? list : void 0;
48767
+ }
48768
+ function convertServerToOpenclaw({ name, config, logger }) {
48769
+ if (declaresNoTransport(config)) return warnAndSkipMcpServer({
48770
+ toolName: "OpenClaw",
48771
+ serverName: name,
48772
+ reason: "no transport",
48773
+ logger
48774
+ });
48775
+ const out = {};
48776
+ if (isRemoteMcpServer(config)) {
48777
+ const url = resolveRemoteMcpUrl(config);
48778
+ if (!url) return warnAndSkipMcpServer({
48779
+ toolName: "OpenClaw",
48780
+ serverName: name,
48781
+ reason: "a remote transport without a url",
48782
+ logger
48783
+ });
48784
+ const stated = config.type ?? config.transport;
48785
+ if (stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) return warnAndSkipMcpServer({
48786
+ toolName: "OpenClaw",
48787
+ serverName: name,
48788
+ reason: "a WebSocket transport, which OpenClaw does not offer (only streamable-http and sse)",
48789
+ logger
48790
+ });
48791
+ out.url = url;
48792
+ out.transport = stated === "sse" ? "sse" : "streamable-http";
48793
+ const headers = copyStringMap(config.headers);
48794
+ if (headers) out.headers = headers;
48795
+ } else {
48796
+ const [command, ...args] = resolveLocalMcpCommand(config);
48797
+ if (!command) return warnAndSkipMcpServer({
48798
+ toolName: "OpenClaw",
48799
+ serverName: name,
48800
+ reason: "a stdio transport without a command",
48801
+ logger
48802
+ });
48803
+ out.command = command;
48804
+ if (args.length > 0) out.args = args;
48805
+ const env = copyStringMap(config.env);
48806
+ if (env) out.env = env;
48807
+ if (typeof config.cwd === "string") out.cwd = config.cwd;
48808
+ }
48809
+ if (config.disabled === true) out.enabled = false;
48810
+ const include = toolFilterList(config.enabledTools);
48811
+ const exclude = toolFilterList(config.disabledTools);
48812
+ if (include || exclude) out.toolFilter = {
48813
+ ...include && { include },
48814
+ ...exclude && { exclude }
48815
+ };
48816
+ copyOpenclawOnlyFields(config, out, {
48817
+ serverName: name,
48818
+ logger
48819
+ });
48820
+ return out;
48821
+ }
48822
+ /**
48823
+ * Converts canonical rulesync servers into OpenClaw `mcp.servers` entries.
48824
+ * Only keys OpenClaw accepts are written — never canonical-only ones such as
48825
+ * `type` or `disabled`, which its strict config validation would answer by
48826
+ * refusing to start the Gateway.
48827
+ */
48828
+ function convertToOpenclawFormat({ mcpServers, logger }) {
48829
+ const result = {};
48830
+ for (const [name, config] of Object.entries(mcpServers)) {
48831
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
48832
+ const converted = convertServerToOpenclaw({
48833
+ name,
48834
+ config,
48835
+ logger
48836
+ });
48837
+ if (converted) result[name] = converted;
48838
+ }
48839
+ return result;
48840
+ }
48841
+ /**
48842
+ * Converts OpenClaw `mcp.servers` entries back into canonical rulesync
48843
+ * servers. OpenClaw-only fields go into the `openclaw.mcpServers` override
48844
+ * block (the whole server, since an override entry replaces the shared one for
48845
+ * that tool), so they survive an import/generate round trip without leaking
48846
+ * into every other tool's config.
48847
+ */
48848
+ function convertFromOpenclawFormat(servers) {
48849
+ const mcpServers = {};
48850
+ const openclawOverrides = {};
48851
+ for (const [name, config] of Object.entries(servers)) {
48852
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
48853
+ const server = {};
48854
+ if (typeof config.command === "string") {
48855
+ server.command = config.command;
48856
+ if (isStringArray$1(config.args)) server.args = config.args;
48857
+ const env = copyStringMap(config.env);
48858
+ if (env) server.env = env;
48859
+ if (typeof config.cwd === "string") server.cwd = config.cwd;
48860
+ } else if (typeof config.url === "string") {
48861
+ server.type = config.transport === "sse" ? "sse" : "http";
48862
+ server.url = config.url;
48863
+ const headers = copyStringMap(config.headers);
48864
+ if (headers) server.headers = headers;
48865
+ }
48866
+ if (config.enabled === false) server.disabled = true;
48867
+ if (isRecord$1(config.toolFilter)) {
48868
+ const include = toolFilterList(config.toolFilter.include);
48869
+ const exclude = toolFilterList(config.toolFilter.exclude);
48870
+ if (include) server.enabledTools = include;
48871
+ if (exclude) server.disabledTools = exclude;
48872
+ }
48873
+ mcpServers[name] = server;
48874
+ const openclawServer = { ...server };
48875
+ if (copyOpenclawOnlyFields(config, openclawServer)) openclawOverrides[name] = openclawServer;
48876
+ }
48877
+ return {
48878
+ mcpServers,
48879
+ openclawOverrides
48880
+ };
48881
+ }
48882
+ /**
48883
+ * OpenClaw MCP servers.
48884
+ *
48885
+ * OpenClaw keeps its MCP registry under `mcp.servers` of the Gateway config
48886
+ * `~/.openclaw/openclaw.json` (JSON5; global only — there is no project-scoped
48887
+ * MCP location). That file also holds credentials and every other Gateway
48888
+ * setting, so generation merges rulesync's servers into `mcp.servers` by name,
48889
+ * keeps servers rulesync does not define and every other key, and never deletes
48890
+ * the file.
48891
+ *
48892
+ * @see https://docs.openclaw.ai/gateway/config-extensions
48893
+ * @see https://docs.openclaw.ai/tools/mcp
48894
+ */
48895
+ var OpenclawMcp = class OpenclawMcp extends ToolMcp {
48896
+ config;
48897
+ constructor(params) {
48898
+ super(params);
48899
+ this.config = parseOpenclawConfig(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
48900
+ }
48901
+ getConfig() {
48902
+ return this.config;
48903
+ }
48904
+ isDeletable() {
48905
+ return false;
48906
+ }
48907
+ static getSettablePaths(_options = {}) {
48908
+ return {
48909
+ relativeDirPath: OPENCLAW_CONFIG_DIR_PATH,
48910
+ relativeFilePath: OPENCLAW_CONFIG_FILE_NAME
48911
+ };
48912
+ }
48913
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
48914
+ if (!global) throw new Error(OPENCLAW_GLOBAL_ONLY_MESSAGE);
48915
+ const paths = this.getSettablePaths({ global });
48916
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
48917
+ return new OpenclawMcp({
48918
+ outputRoot,
48919
+ relativeDirPath: paths.relativeDirPath,
48920
+ relativeFilePath: paths.relativeFilePath,
48921
+ fileContent,
48922
+ validate,
48923
+ global
48924
+ });
48925
+ }
48926
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
48927
+ if (!global) throw new Error(OPENCLAW_GLOBAL_ONLY_MESSAGE);
48928
+ const paths = this.getSettablePaths({ global });
48929
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
48930
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
48931
+ const existing = parseOpenclawConfig(existingContent, filePath);
48932
+ const existingMcp = isRecord$1(existing["mcp"]) ? existing["mcp"] : {};
48933
+ const existingServers = isRecord$1(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
48934
+ const converted = convertToOpenclawFormat({
48935
+ mcpServers: rulesyncMcp.getMcpServers(),
48936
+ logger
48937
+ });
48938
+ const servers = {
48939
+ ...existingServers,
48940
+ ...converted
48941
+ };
48942
+ const kept = Object.keys(existingServers).filter((name) => !PROTOTYPE_POLLUTION_KEYS.has(name) && !Object.hasOwn(converted, name));
48943
+ if (kept.length > 0) logger?.warn(`OpenClaw MCP servers not defined in rulesync are kept in ${join(paths.relativeDirPath, paths.relativeFilePath)}: ${kept.map((name) => quoteValueForWarning(name)).join(", ")}. Remove them with 'openclaw mcp unset <name>' if they are no longer wanted.`);
48944
+ return new OpenclawMcp({
48945
+ outputRoot,
48946
+ relativeDirPath: paths.relativeDirPath,
48947
+ relativeFilePath: paths.relativeFilePath,
48948
+ fileContent: applySharedConfigPatch({
48949
+ fileKey: sharedConfigFileKey(paths),
48950
+ feature: "mcp",
48951
+ existingContent,
48952
+ patch: { ["mcp"]: {
48953
+ ...existingMcp,
48954
+ [OPENCLAW_MCP_SERVERS_KEY]: servers
48955
+ } },
48956
+ filePath,
48957
+ logger
48958
+ }),
48959
+ validate,
48960
+ global
48961
+ });
48962
+ }
48963
+ toRulesyncMcp() {
48964
+ const mcp = isRecord$1(this.config["mcp"]) ? this.config["mcp"] : {};
48965
+ const { mcpServers, openclawOverrides } = convertFromOpenclawFormat(isRecord$1(mcp["servers"]) ? mcp[OPENCLAW_MCP_SERVERS_KEY] : {});
48966
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
48967
+ mcpServers,
48968
+ ...Object.keys(openclawOverrides).length > 0 && { openclaw: { mcpServers: openclawOverrides } }
48969
+ }, null, 2) });
48970
+ }
48971
+ validate() {
48972
+ return {
48973
+ success: true,
48974
+ error: null
48975
+ };
48976
+ }
48977
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
48978
+ return new OpenclawMcp({
48979
+ outputRoot,
48980
+ relativeDirPath,
48981
+ relativeFilePath,
48982
+ fileContent: "",
48983
+ validate: false,
48984
+ global
48985
+ });
48986
+ }
48987
+ };
48988
+ //#endregion
47793
48989
  //#region src/features/mcp/pi-mcp.ts
47794
48990
  /**
47795
48991
  * Canonical keys consumed by the conversion below (or with no Pi counterpart),
@@ -50844,6 +52040,128 @@ function asZcodeRemoteType(stated, url) {
50844
52040
  if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
50845
52041
  }
50846
52042
  /**
52043
+ * The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
52044
+ * remote server, and which of them are required. Read off the 3.14.3/3.15.3
52045
+ * runtime config schema, since ZCode's MCP page only says to "enable OAuth for
52046
+ * the service in its configuration". Both variants are strict, so any other
52047
+ * key would make ZCode skip the server.
52048
+ */
52049
+ const ZCODE_OAUTH_FLOWS = {
52050
+ client_credentials: {
52051
+ required: ["clientId", "clientSecret"],
52052
+ optional: ["clientName", "scope"]
52053
+ },
52054
+ authorization_code: {
52055
+ required: [],
52056
+ optional: [
52057
+ "clientId",
52058
+ "clientSecret",
52059
+ "clientName",
52060
+ "redirectPath",
52061
+ "scope"
52062
+ ]
52063
+ }
52064
+ };
52065
+ /**
52066
+ * The `protocolVersion` values ZCode's server schema accepts (an enum on a
52067
+ * strict object, so any other value makes ZCode skip the server).
52068
+ */
52069
+ const ZCODE_PROTOCOL_VERSIONS = /* @__PURE__ */ new Set([
52070
+ "auto",
52071
+ "legacy",
52072
+ "2026-07-28"
52073
+ ]);
52074
+ /**
52075
+ * The namespaced canonical key that carries ZCode's `oauth`. The canonical
52076
+ * `oauth` already holds Claude Code's `{ clientId }` object, and ZCode's shape
52077
+ * may carry a client secret, so it is kept apart (the `crushOauth*`
52078
+ * precedent) and stripped by `RulesyncMcp.getMcpServers()`.
52079
+ */
52080
+ const ZCODE_OAUTH_CANONICAL_KEY = "zcodeOauth";
52081
+ /**
52082
+ * Whether an `oauth` value names one of ZCode's flows.
52083
+ */
52084
+ function isZcodeOauth(value) {
52085
+ return isRecord$1(value) && typeof value.type === "string" && value.type in ZCODE_OAUTH_FLOWS;
52086
+ }
52087
+ /**
52088
+ * Build ZCode's `oauth` from a `zcodeOauth` value: only the keys of the named
52089
+ * flow are kept, as strings. A value naming no ZCode flow, or missing a key
52090
+ * the flow requires, is dropped with a warning instead of making ZCode skip
52091
+ * the server.
52092
+ */
52093
+ function toZcodeOauth({ serverName, value, logger }) {
52094
+ if (value === void 0) return void 0;
52095
+ if (!isZcodeOauth(value)) {
52096
+ logger?.warn(`ZCode MCP server "${serverName}": dropping zcodeOauth, as its type must be "client_credentials" or "authorization_code".`);
52097
+ return;
52098
+ }
52099
+ const type = String(value.type);
52100
+ const flow = ZCODE_OAUTH_FLOWS[type];
52101
+ if (!flow) return void 0;
52102
+ const missing = flow.required.filter((key) => typeof value[key] !== "string");
52103
+ if (missing.length > 0) {
52104
+ logger?.warn(`ZCode MCP server "${serverName}": dropping zcodeOauth, as the ${type} flow requires ${missing.join(", ")}.`);
52105
+ return;
52106
+ }
52107
+ const allowed = /* @__PURE__ */ new Set([...flow.required, ...flow.optional]);
52108
+ const oauth = { type };
52109
+ const ignored = [];
52110
+ for (const [key, entry] of Object.entries(value)) {
52111
+ if (key === "type" || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
52112
+ if (allowed.has(key) && typeof entry === "string") oauth[key] = entry;
52113
+ else ignored.push(key);
52114
+ }
52115
+ if (ignored.length > 0) logger?.warn(`ZCode MCP server "${serverName}": ignoring zcodeOauth keys the ${type} flow does not accept: ${ignored.join(", ")}.`);
52116
+ return oauth;
52117
+ }
52118
+ /**
52119
+ * The `zcodeOauth` a server was authored with. `getMcpServers()` strips it, so
52120
+ * the authored server (`rawServerOf`) is consulted first.
52121
+ */
52122
+ function authoredZcodeOauth({ name, config, rawServerOf }) {
52123
+ const raw = rawServerOf?.(name);
52124
+ return isRecord$1(raw) ? raw[ZCODE_OAUTH_CANONICAL_KEY] : config[ZCODE_OAUTH_CANONICAL_KEY];
52125
+ }
52126
+ function isPositiveMs(value) {
52127
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
52128
+ }
52129
+ /**
52130
+ * Resolve ZCode's per-server `timeoutMs` (a positive number of milliseconds):
52131
+ * a valid explicit `timeoutMs` wins, otherwise the canonical `timeout`
52132
+ * (milliseconds, the `gitlabduo-mcp.ts` precedent) is used. A value ZCode
52133
+ * would reject is dropped with a warning instead of making ZCode skip the
52134
+ * server.
52135
+ */
52136
+ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
52137
+ const { timeoutMs, timeout } = config;
52138
+ if (timeoutMs !== void 0) {
52139
+ if (isPositiveMs(timeoutMs)) return timeoutMs;
52140
+ logger?.warn(`ZCode MCP server "${serverName}": ignoring timeoutMs ${JSON.stringify(timeoutMs)}, as it must be a positive number of milliseconds.`);
52141
+ }
52142
+ if (timeout === void 0) return void 0;
52143
+ if (isPositiveMs(timeout)) return timeout;
52144
+ logger?.warn(`ZCode MCP server "${serverName}": dropping timeout ${JSON.stringify(timeout)}, as timeoutMs must be a positive number of milliseconds.`);
52145
+ }
52146
+ /**
52147
+ * The per-server keys ZCode reads on every transport: `timeoutMs`,
52148
+ * `protocolVersion` and the enable toggle (`enableKey`).
52149
+ */
52150
+ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
52151
+ const common = {};
52152
+ const timeoutMs = resolveZcodeTimeoutMs({
52153
+ serverName,
52154
+ config,
52155
+ logger
52156
+ });
52157
+ if (timeoutMs !== void 0) common.timeoutMs = timeoutMs;
52158
+ const { protocolVersion } = config;
52159
+ if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
52160
+ else if (protocolVersion !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping protocolVersion ${JSON.stringify(protocolVersion)}, as ZCode accepts only ${[...ZCODE_PROTOCOL_VERSIONS].map((v) => JSON.stringify(v)).join(", ")}.`);
52161
+ if (config.disabled === true) common[enableKey] = false;
52162
+ return common;
52163
+ }
52164
+ /**
50847
52165
  * Convert canonical rulesync servers to ZCode's native `mcp.servers` shape:
50848
52166
  * stdio servers carry `command`/`args`/`env`, remote servers carry
50849
52167
  * `type` (`http` or `sse`), `url` and optional `headers`. A canonical
@@ -50851,9 +52169,18 @@ function asZcodeRemoteType(stated, url) {
50851
52169
  * plugin's `.mcp.json`, via `enableKey`), which it defaults to `true` when
50852
52170
  * absent.
50853
52171
  *
52172
+ * Every server also carries `timeoutMs` (from the canonical `timeout`, or an
52173
+ * explicit `timeoutMs`) and an allow-listed `protocolVersion`; stdio servers
52174
+ * carry `cwd`, and remote servers an `oauth` built from the namespaced
52175
+ * `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
52176
+ * is read from the authored server through `rawServerOf`. ZCode's docs do not
52177
+ * name these keys, so they follow the 3.14.3/3.15.3 runtime's per-server
52178
+ * schema, which is strict: an unknown key makes ZCode skip the server, which
52179
+ * is why the output stays a whitelist.
52180
+ *
50854
52181
  * @see https://zcode.z.ai/en/docs/mcp-services
50855
52182
  */
50856
- function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
52183
+ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable", rawServerOf }) {
50857
52184
  const result = {};
50858
52185
  for (const [name, config] of Object.entries(mcpServers)) {
50859
52186
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
@@ -50893,6 +52220,16 @@ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
50893
52220
  converted.type = type;
50894
52221
  converted.url = url;
50895
52222
  if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
52223
+ const oauth = toZcodeOauth({
52224
+ serverName: name,
52225
+ value: authoredZcodeOauth({
52226
+ name,
52227
+ config,
52228
+ rawServerOf
52229
+ }),
52230
+ logger
52231
+ });
52232
+ if (oauth !== void 0) converted.oauth = oauth;
50896
52233
  } else {
50897
52234
  const [command, ...args] = resolveLocalMcpCommand(config);
50898
52235
  if (!command) {
@@ -50907,35 +52244,62 @@ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
50907
52244
  converted.command = command;
50908
52245
  converted.args = args;
50909
52246
  if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
52247
+ if (typeof config.cwd === "string" && config.cwd !== "") converted.cwd = config.cwd;
50910
52248
  }
50911
- if (config.disabled === true) converted[enableKey] = false;
50912
- result[name] = converted;
52249
+ result[name] = {
52250
+ ...converted,
52251
+ ...toZcodeCommonKeys({
52252
+ serverName: name,
52253
+ config,
52254
+ enableKey,
52255
+ logger
52256
+ })
52257
+ };
50913
52258
  }
50914
52259
  return result;
50915
52260
  }
50916
52261
  /**
52262
+ * ZCode's per-server enable toggle, in both spellings: the runtime normalizes
52263
+ * the legacy `enable` to `enabled`, and a plugin's `.mcp.json` uses `enabled`.
52264
+ */
52265
+ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
52266
+ /**
50917
52267
  * Convert ZCode's native `mcp.servers` shape back to canonical rulesync
50918
- * servers: `enable: false` maps back to `disabled: true`, and unknown keys pass
50919
- * through untouched so an import keeps whatever a hand-authored entry declared.
50920
- * Generation back out is a whitelist, so only the keys `convertToZcodeFormat`
50921
- * writes are re-emitted.
52268
+ * servers: `enable: false` / `enabled: false` map back to `disabled: true`,
52269
+ * and unknown keys pass through untouched so an import keeps whatever a
52270
+ * hand-authored entry declared. `timeoutMs` is lifted back into the canonical
52271
+ * `timeout` (milliseconds); when the server also states a `timeout`, ZCode
52272
+ * has already dropped that one, so `timeoutMs` replaces it. A ZCode-shaped
52273
+ * `oauth` is imported as the namespaced `zcodeOauth`, so its client secret is
52274
+ * never emitted to another tool. Generation back out is a whitelist, so only
52275
+ * the keys `convertToZcodeFormat` writes are re-emitted.
50922
52276
  *
50923
52277
  * An explicit `enable: true` is dropped rather than mapped to a canonical
50924
52278
  * `disabled: false`: absent already means enabled on both sides, so the flag
50925
52279
  * carries no information, and emitting it would push a redundant `disabled`
50926
- * key into every other tool's generated config.
52280
+ * key into every other tool's generated config. (A canonical `enabled` is
52281
+ * rulesync's own generation filter, so it must never be passed through.)
50927
52282
  */
50928
- function convertFromZcodeFormat({ zcodeServers, enableKey = "enable" }) {
52283
+ function convertFromZcodeFormat({ zcodeServers }) {
50929
52284
  const result = {};
50930
52285
  for (const [name, config] of Object.entries(zcodeServers)) {
50931
52286
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
50932
52287
  const converted = {};
50933
52288
  for (const [key, value] of Object.entries(config)) {
50934
52289
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
50935
- if (key === enableKey) {
52290
+ if (ZCODE_ENABLE_KEYS.has(key)) {
50936
52291
  if (value === false) converted.disabled = true;
50937
52292
  continue;
50938
52293
  }
52294
+ if (key === "timeoutMs" && typeof value === "number") {
52295
+ converted.timeout = value;
52296
+ continue;
52297
+ }
52298
+ if (key === "timeout" && typeof config.timeoutMs === "number") continue;
52299
+ if (key === "oauth" && isZcodeOauth(value)) {
52300
+ converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
52301
+ continue;
52302
+ }
50939
52303
  converted[key] = value;
50940
52304
  }
50941
52305
  result[name] = converted;
@@ -50992,7 +52356,8 @@ var ZcodeMcp = class ZcodeMcp extends ToolMcp {
50992
52356
  const existing = parseZcodeConfig(existingContent, filePath);
50993
52357
  const converted = convertToZcodeFormat({
50994
52358
  mcpServers: rulesyncMcp.getMcpServers(),
50995
- logger
52359
+ logger,
52360
+ rawServerOf: (name) => rulesyncMcp.getRawMcpServer(name)
50996
52361
  });
50997
52362
  const existingMcp = isRecord$1(existing["mcp"]) ? existing["mcp"] : {};
50998
52363
  return new ZcodeMcp({
@@ -51096,7 +52461,8 @@ var ZcodePluginMcp = class ZcodePluginMcp extends ToolMcp {
51096
52461
  const mcpServers = convertToZcodeFormat({
51097
52462
  mcpServers: rulesyncMcp.getMcpServers(),
51098
52463
  logger,
51099
- enableKey: ZCODE_PLUGIN_ENABLE_KEY
52464
+ enableKey: ZCODE_PLUGIN_ENABLE_KEY,
52465
+ rawServerOf: (name) => rulesyncMcp.getRawMcpServer(name)
51100
52466
  });
51101
52467
  return new ZcodePluginMcp({
51102
52468
  outputRoot,
@@ -51107,10 +52473,7 @@ var ZcodePluginMcp = class ZcodePluginMcp extends ToolMcp {
51107
52473
  });
51108
52474
  }
51109
52475
  toRulesyncMcp() {
51110
- const mcpServers = convertFromZcodeFormat({
51111
- zcodeServers: serversOf(this.json),
51112
- enableKey: ZCODE_PLUGIN_ENABLE_KEY
51113
- });
52476
+ const mcpServers = convertFromZcodeFormat({ zcodeServers: serversOf(this.json) });
51114
52477
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
51115
52478
  }
51116
52479
  validate() {
@@ -51615,6 +52978,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
51615
52978
  supportsDisabledTools: true
51616
52979
  }
51617
52980
  }],
52981
+ ["openclaw", {
52982
+ class: OpenclawMcp,
52983
+ meta: {
52984
+ supportsProject: false,
52985
+ supportsGlobal: true,
52986
+ supportsEnabledTools: true,
52987
+ supportsDisabledTools: true
52988
+ }
52989
+ }],
51618
52990
  ["kimi-code", {
51619
52991
  class: KimiCodeMcp,
51620
52992
  meta: {
@@ -52768,7 +54140,7 @@ function parsedGlobsIntersect(left, right, budget) {
52768
54140
  *
52769
54141
  * @see https://antigravity.google/docs/permissions?tab=cli
52770
54142
  */
52771
- const REGEX_PREFIX = "regex:";
54143
+ const REGEX_PREFIX$1 = "regex:";
52772
54144
  /**
52773
54145
  * The Antigravity actions whose target is a command: `command`, and
52774
54146
  * `unsandboxed`, which takes the same word-by-word prefix or `regex:` target.
@@ -53106,7 +54478,7 @@ function wordToRegexSource(word) {
53106
54478
  * pattern does.
53107
54479
  */
53108
54480
  function toAntigravityCommandTarget(pattern) {
53109
- if (pattern.startsWith(REGEX_PREFIX)) {
54481
+ if (pattern.startsWith(REGEX_PREFIX$1)) {
53110
54482
  const regexWords = splitWords(pattern.slice(6));
53111
54483
  if (regexWords.length === 0) return { skipReason: "The regex is empty" };
53112
54484
  const invalid = regexWords.find(isInvalidRegex);
@@ -53126,7 +54498,7 @@ function toAntigravityCommandTarget(pattern) {
53126
54498
  const head = words.slice(0, -1);
53127
54499
  if (head.length > 0 && isStarWord(words.at(-1) ?? []) && head.every(isLiteralWord)) return { target: head.map(wordToLiteral).join(" ") };
53128
54500
  return {
53129
- target: `${REGEX_PREFIX}${words.map(wordToRegexSource).join(" ")}`,
54501
+ target: `${REGEX_PREFIX$1}${words.map(wordToRegexSource).join(" ")}`,
53130
54502
  note
53131
54503
  };
53132
54504
  }
@@ -53138,7 +54510,7 @@ function toAntigravityCommandTarget(pattern) {
53138
54510
  * word with a brace may be part of `{a,b}` alternatives, so it ends the prefix.
53139
54511
  */
53140
54512
  function toAntigravityCommandLiteralPrefix(pattern) {
53141
- if (pattern.startsWith(REGEX_PREFIX)) return;
54513
+ if (pattern.startsWith(REGEX_PREFIX$1)) return;
53142
54514
  const words = splitIntoWords(parseGlobPattern(pattern).steps);
53143
54515
  const prefix = [];
53144
54516
  for (const word of words) {
@@ -53146,7 +54518,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
53146
54518
  if (literal === void 0 || literal.includes("{") || literal.includes("}")) break;
53147
54519
  prefix.push(literal);
53148
54520
  }
53149
- if (prefix.length === 0 || prefix[0]?.startsWith(REGEX_PREFIX)) return;
54521
+ if (prefix.length === 0 || prefix[0]?.startsWith(REGEX_PREFIX$1)) return;
53150
54522
  return prefix.join(" ");
53151
54523
  }
53152
54524
  /**
@@ -53197,11 +54569,11 @@ function regexToGlob(source) {
53197
54569
  * `regex:` target is kept as is.
53198
54570
  */
53199
54571
  function fromAntigravityCommandTarget(target) {
53200
- if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
54572
+ if (target.startsWith(REGEX_PREFIX$1)) return regexToGlob(target.slice(6)) ?? target;
53201
54573
  if (target === "*") return target;
53202
54574
  if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
53203
54575
  const words = splitWords(target);
53204
- return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
54576
+ return `${REGEX_PREFIX$1}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
53205
54577
  }
53206
54578
  return `${target} *`;
53207
54579
  }
@@ -53221,7 +54593,7 @@ function fromAntigravityCommandTarget(target) {
53221
54593
  * @see https://antigravity.google/docs/permissions
53222
54594
  */
53223
54595
  function describeAntigravityCommandImportLoss(target) {
53224
- if (target.startsWith(REGEX_PREFIX) || target === "*") return;
54596
+ if (target.startsWith(REGEX_PREFIX$1) || target === "*") return;
53225
54597
  if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
53226
54598
  reason: "it holds characters that are special in a glob (`*`, `?`, `[`, `{` or `\\`), which Antigravity matches literally (likely output of an older rulesync), so it is imported as a literal-match regex that no other tool enforces",
53227
54599
  denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
@@ -55123,7 +56495,7 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
55123
56495
  //#region src/features/permissions/bob-permissions.ts
55124
56496
  const TOOL_LABEL = "IBM Bob";
55125
56497
  /** The top-level settings key holding Bob's auto-approval configuration. */
55126
- const APPROVAL_KEY = "approval";
56498
+ const APPROVAL_KEY$1 = "approval";
55127
56499
  /** The top-level settings key holding Bob's per-feature auto-approve toggles. */
55128
56500
  const AUTO_APPROVE_KEY = "autoApprove";
55129
56501
  /** The tool permission groups Bob documents for `approval.allowed_permissions`. */
@@ -55144,7 +56516,7 @@ const BOB_AUTO_APPROVE_KEYS = /* @__PURE__ */ new Set(["skills"]);
55144
56516
  const OUTSIDE_WORKSPACE_GROUP_ID = "read";
55145
56517
  /** The only executor tool Bob's `allowedExecutors` list documents. */
55146
56518
  const EXECUTE_COMMAND_TOOL_ID = "execute_command";
55147
- const SURFACE_LABEL = `${APPROVAL_KEY}.allowedExecutors[${EXECUTE_COMMAND_TOOL_ID}].approvedCommands/deniedCommands`;
56519
+ const SURFACE_LABEL = `${APPROVAL_KEY$1}.allowedExecutors[${EXECUTE_COMMAND_TOOL_ID}].approvedCommands/deniedCommands`;
55148
56520
  function asStringArray$2(value) {
55149
56521
  if (!Array.isArray(value)) return [];
55150
56522
  return value.filter((entry) => typeof entry === "string");
@@ -55236,7 +56608,7 @@ function warnAboutUndocumentedSettings({ override, logger }) {
55236
56608
  * fetched permissions file. Switches that are already on are not repeated.
55237
56609
  */
55238
56610
  function warnAboutNewAutoApprovals({ settings, override, filePath, logger }) {
55239
- const existingApproval = isPlainObject$1(settings[APPROVAL_KEY]) ? settings[APPROVAL_KEY] : {};
56611
+ const existingApproval = isPlainObject$1(settings[APPROVAL_KEY$1]) ? settings[APPROVAL_KEY$1] : {};
55240
56612
  const existingGroups = new Set(asStringArray$2(existingApproval.allowed_permissions));
55241
56613
  const existingOutside = new Set((Array.isArray(existingApproval.permissionOptions) ? existingApproval.permissionOptions : []).filter(isPlainObject$1).filter((option) => option.enableOutsideWorkspace === true).map((option) => option.groupId));
55242
56614
  const existingAutoApprove = isPlainObject$1(settings[AUTO_APPROVE_KEY]) ? settings[AUTO_APPROVE_KEY] : {};
@@ -55302,7 +56674,7 @@ function buildBobCommandLists({ permission, logger }) {
55302
56674
  */
55303
56675
  function readBobOverride(settings) {
55304
56676
  const override = {};
55305
- const approval = settings[APPROVAL_KEY];
56677
+ const approval = settings[APPROVAL_KEY$1];
55306
56678
  if (isPlainObject$1(approval)) {
55307
56679
  const overrideApproval = {};
55308
56680
  const groups = approval.allowed_permissions;
@@ -55431,8 +56803,8 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
55431
56803
  deniedCommands: denied
55432
56804
  };
55433
56805
  }
55434
- patch[APPROVAL_KEY] = buildApproval({
55435
- existingApproval: settings[APPROVAL_KEY],
56806
+ patch[APPROVAL_KEY$1] = buildApproval({
56807
+ existingApproval: settings[APPROVAL_KEY$1],
55436
56808
  overrideApproval,
55437
56809
  commandLists
55438
56810
  });
@@ -55461,7 +56833,7 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
55461
56833
  fileContent: this.getFileContent(),
55462
56834
  filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
55463
56835
  });
55464
- const approval = settings[APPROVAL_KEY];
56836
+ const approval = settings[APPROVAL_KEY$1];
55465
56837
  const entry = (isPlainObject$1(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
55466
56838
  const rules = {};
55467
56839
  for (const pattern of asStringArray$2(entry?.approvedCommands)) rules[pattern] = "allow";
@@ -57177,7 +58549,7 @@ const CODEBUDDY_RULE_LISTS = [
57177
58549
  "ask",
57178
58550
  "deny"
57179
58551
  ];
57180
- const CATCH_ALL_PATTERN$12 = "*";
58552
+ const CATCH_ALL_PATTERN$13 = "*";
57181
58553
  const MCP_CATEGORY = "mcp";
57182
58554
  const MCP_RULE_PREFIX = "mcp__";
57183
58555
  const ALL_MCP_RULE = "mcp__*";
@@ -57204,7 +58576,7 @@ const CATEGORY_TO_CODEBUDDY_TOOL = {
57204
58576
  skill: "Skill"
57205
58577
  };
57206
58578
  const CODEBUDDY_TOOL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_CODEBUDDY_TOOL).map(([category, tool]) => [tool, category]));
57207
- function toStringList$2(value) {
58579
+ function toStringList$3(value) {
57208
58580
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
57209
58581
  }
57210
58582
  /**
@@ -57250,7 +58622,7 @@ function categoryOfRule$2(rule) {
57250
58622
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
57251
58623
  */
57252
58624
  function buildCodebuddyRule({ category, pattern, action, logger }) {
57253
- const catchAll = pattern === CATCH_ALL_PATTERN$12;
58625
+ const catchAll = pattern === CATCH_ALL_PATTERN$13;
57254
58626
  if (category === MCP_CATEGORY) {
57255
58627
  if (catchAll && action === "allow") {
57256
58628
  logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
@@ -57259,7 +58631,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
57259
58631
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
57260
58632
  }
57261
58633
  if (category === "*") {
57262
- if (catchAll) return CATCH_ALL_PATTERN$12;
58634
+ if (catchAll) return CATCH_ALL_PATTERN$13;
57263
58635
  logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
57264
58636
  return null;
57265
58637
  }
@@ -57277,7 +58649,7 @@ function parseCodebuddyRule(rule) {
57277
58649
  const { specifier } = splitCodebuddyRule(rule);
57278
58650
  if (specifier === void 0) return {
57279
58651
  category,
57280
- pattern: CATCH_ALL_PATTERN$12
58652
+ pattern: CATCH_ALL_PATTERN$13
57281
58653
  };
57282
58654
  const trimmed = specifier.trim();
57283
58655
  return trimmed === "" ? null : {
@@ -57318,7 +58690,7 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
57318
58690
  ask: [],
57319
58691
  deny: []
57320
58692
  };
57321
- for (const list of CODEBUDDY_RULE_LISTS) lists[list] = toStringList$2(existingPermissions[list]).filter((rule) => {
58693
+ for (const list of CODEBUDDY_RULE_LISTS) lists[list] = toStringList$3(existingPermissions[list]).filter((rule) => {
57322
58694
  const category = categoryOfRule$2(rule);
57323
58695
  return category === void 0 || !managedCategories.has(category);
57324
58696
  });
@@ -57409,7 +58781,7 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
57409
58781
  });
57410
58782
  const permissions = isRecord$1(settings[CODEBUDDY_PERMISSIONS_KEY]) ? settings[CODEBUDDY_PERMISSIONS_KEY] : {};
57411
58783
  const permission = {};
57412
- for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringList$2(permissions[action])) {
58784
+ for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringList$3(permissions[action])) {
57413
58785
  const parsed = parseCodebuddyRule(rule);
57414
58786
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
57415
58787
  const rules = permission[parsed.category] ??= {};
@@ -59528,7 +60900,7 @@ function mapBashActionToDecision(action) {
59528
60900
  //#region src/features/permissions/commandcode-permissions.ts
59529
60901
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
59530
60902
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
59531
- const CATCH_ALL_PATTERN$11 = "*";
60903
+ const CATCH_ALL_PATTERN$12 = "*";
59532
60904
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
59533
60905
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
59534
60906
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -59553,14 +60925,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
59553
60925
  web_fetch: "webfetch",
59554
60926
  web_search: "websearch"
59555
60927
  };
59556
- const ACTION_RANK$5 = {
60928
+ const ACTION_RANK$6 = {
59557
60929
  allow: 0,
59558
60930
  ask: 1,
59559
60931
  deny: 2
59560
60932
  };
59561
60933
  /** Whether `action` outranks `existing` (deny > ask > allow). */
59562
60934
  function isStricterAction({ action, existing }) {
59563
- return existing === void 0 || ACTION_RANK$5[action] > ACTION_RANK$5[existing];
60935
+ return existing === void 0 || ACTION_RANK$6[action] > ACTION_RANK$6[existing];
59564
60936
  }
59565
60937
  /**
59566
60938
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -59583,10 +60955,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
59583
60955
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
59584
60956
  */
59585
60957
  function buildCommandcodeRule(category, pattern) {
59586
- const catchAll = pattern === CATCH_ALL_PATTERN$11 || pattern === "";
60958
+ const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
59587
60959
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
59588
60960
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
59589
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$11 : null;
60961
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
59590
60962
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
59591
60963
  if (tool === void 0) return null;
59592
60964
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -59622,7 +60994,7 @@ function splitCommandcodeRule(rule) {
59622
60994
  }
59623
60995
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
59624
60996
  function isCatchAllSpecifier(inner) {
59625
- return inner === "" || inner === CATCH_ALL_PATTERN$11;
60997
+ return inner === "" || inner === CATCH_ALL_PATTERN$12;
59626
60998
  }
59627
60999
  /**
59628
61000
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -59656,34 +61028,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
59656
61028
  if (server.length === 0) return null;
59657
61029
  const everyServer = {
59658
61030
  category: "mcp",
59659
- pattern: CATCH_ALL_PATTERN$11,
61031
+ pattern: CATCH_ALL_PATTERN$12,
59660
61032
  notInAllow: true
59661
61033
  };
59662
61034
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
59663
- if (server === CATCH_ALL_PATTERN$11) return everyServer;
59664
- if (server.includes(CATCH_ALL_PATTERN$11)) return null;
61035
+ if (server === CATCH_ALL_PATTERN$12) return everyServer;
61036
+ if (server.includes(CATCH_ALL_PATTERN$12)) return null;
59665
61037
  return {
59666
- category: rest === "" || rest === CATCH_ALL_PATTERN$11 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
59667
- pattern: CATCH_ALL_PATTERN$11,
61038
+ category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
61039
+ pattern: CATCH_ALL_PATTERN$12,
59668
61040
  notInAllow: false
59669
61041
  };
59670
61042
  }
59671
61043
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
59672
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$11);
61044
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
59673
61045
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
59674
61046
  category,
59675
61047
  pattern: inner,
59676
61048
  notInAllow: true
59677
61049
  } : null;
59678
- if (remainder === CATCH_ALL_PATTERN$11) return everyServer;
59679
- if (rest === CATCH_ALL_PATTERN$11 && !server.includes(CATCH_ALL_PATTERN$11)) return {
61050
+ if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
61051
+ if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
59680
61052
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
59681
- pattern: CATCH_ALL_PATTERN$11,
61053
+ pattern: CATCH_ALL_PATTERN$12,
59682
61054
  notInAllow: true
59683
61055
  };
59684
61056
  return namesTool ? {
59685
61057
  category,
59686
- pattern: CATCH_ALL_PATTERN$11,
61058
+ pattern: CATCH_ALL_PATTERN$12,
59687
61059
  notInAllow: false
59688
61060
  } : null;
59689
61061
  }
@@ -59707,14 +61079,14 @@ function friendlyToolRule({ category, inner }) {
59707
61079
  if (category === "bash") {
59708
61080
  if (isCatchAllSpecifier(inner)) return {
59709
61081
  category,
59710
- pattern: CATCH_ALL_PATTERN$11,
61082
+ pattern: CATCH_ALL_PATTERN$12,
59711
61083
  notInAllow: false
59712
61084
  };
59713
61085
  const normalized = inner.trim().replace(/\s+/g, " ");
59714
61086
  if (normalized === "" || normalized === ":*") return null;
59715
61087
  return isCatchAllSpecifier(normalized) ? {
59716
61088
  category,
59717
- pattern: CATCH_ALL_PATTERN$11,
61089
+ pattern: CATCH_ALL_PATTERN$12,
59718
61090
  notInAllow: true
59719
61091
  } : {
59720
61092
  category,
@@ -59724,7 +61096,7 @@ function friendlyToolRule({ category, inner }) {
59724
61096
  }
59725
61097
  if (isCatchAllSpecifier(inner)) return {
59726
61098
  category,
59727
- pattern: CATCH_ALL_PATTERN$11,
61099
+ pattern: CATCH_ALL_PATTERN$12,
59728
61100
  notInAllow: false
59729
61101
  };
59730
61102
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -59743,9 +61115,9 @@ function friendlyToolRule({ category, inner }) {
59743
61115
  */
59744
61116
  function parseCommandcodeRule(rule) {
59745
61117
  const { tool, inner } = splitCommandcodeRule(rule);
59746
- if (tool === CATCH_ALL_PATTERN$11) return isCatchAllSpecifier(inner) ? {
61118
+ if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
59747
61119
  category: "*",
59748
- pattern: CATCH_ALL_PATTERN$11,
61120
+ pattern: CATCH_ALL_PATTERN$12,
59749
61121
  notInAllow: true
59750
61122
  } : null;
59751
61123
  const lowered = tool.toLowerCase();
@@ -59815,7 +61187,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
59815
61187
  }).filter((rule) => {
59816
61188
  const parsed = parseCommandcodeRule(rule);
59817
61189
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
59818
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$11})`);
61190
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$12})`);
59819
61191
  if (key !== "allow" && isStricterAction({
59820
61192
  action: key,
59821
61193
  existing: replacement
@@ -59872,7 +61244,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
59872
61244
  const ranked = /* @__PURE__ */ new Map();
59873
61245
  const managedCategories = /* @__PURE__ */ new Set();
59874
61246
  for (const [category, rules] of Object.entries(permission)) {
59875
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$11);
61247
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
59876
61248
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
59877
61249
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
59878
61250
  for (const [pattern, action] of Object.entries(rules)) {
@@ -60143,7 +61515,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
60143
61515
  //#endregion
60144
61516
  //#region src/features/permissions/continue-permissions.ts
60145
61517
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
60146
- const CATCH_ALL_PATTERN$10 = "*";
61518
+ const CATCH_ALL_PATTERN$11 = "*";
60147
61519
  /**
60148
61520
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
60149
61521
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -60151,7 +61523,7 @@ const CATCH_ALL_PATTERN$10 = "*";
60151
61523
  * the bare entry instead of being skipped as a pattern-specific rule.
60152
61524
  */
60153
61525
  function normalizeCatchAllPattern$1(pattern) {
60154
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
61526
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
60155
61527
  }
60156
61528
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
60157
61529
  bash: "Bash",
@@ -60188,7 +61560,7 @@ function toCanonicalToolName$6(continueName) {
60188
61560
  * `Tool(pattern)`.
60189
61561
  */
60190
61562
  function buildContinuePermissionEntry(toolName, pattern) {
60191
- return pattern === CATCH_ALL_PATTERN$10 ? toolName : `${toolName}(${pattern})`;
61563
+ return pattern === CATCH_ALL_PATTERN$11 ? toolName : `${toolName}(${pattern})`;
60192
61564
  }
60193
61565
  /**
60194
61566
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -60202,7 +61574,7 @@ function parseContinuePermissionEntry(entry) {
60202
61574
  if (toolName === "") return;
60203
61575
  return {
60204
61576
  toolName,
60205
- pattern: pattern === "" ? CATCH_ALL_PATTERN$10 : pattern
61577
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
60206
61578
  };
60207
61579
  }
60208
61580
  /**
@@ -60376,7 +61748,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
60376
61748
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
60377
61749
  continue;
60378
61750
  }
60379
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$10) {
61751
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
60380
61752
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
60381
61753
  continue;
60382
61754
  }
@@ -60808,7 +62180,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60808
62180
  * arrays are accepted only in MDM/enterprise managed settings, and session
60809
62181
  * tool approvals are machine-written to `permissions-config.json`.
60810
62182
  */
60811
- const WEBFETCH_CATEGORY = "webfetch";
62183
+ const WEBFETCH_CATEGORY$1 = "webfetch";
60812
62184
  /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
60813
62185
  const ALLOWED_URLS_KEY = "allowedUrls";
60814
62186
  const DENIED_URLS_KEY = "deniedUrls";
@@ -60895,7 +62267,7 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
60895
62267
  const paths = CopilotcliPermissions.getSettablePaths({ global });
60896
62268
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
60897
62269
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
60898
- const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
62270
+ const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY$1];
60899
62271
  const patch = {};
60900
62272
  if (rules !== void 0) {
60901
62273
  const { allowedUrls, deniedUrls } = splitUrlRules(rules);
@@ -60923,7 +62295,7 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
60923
62295
  const rules = {};
60924
62296
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
60925
62297
  if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
60926
- const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
62298
+ const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
60927
62299
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
60928
62300
  }
60929
62301
  parseSettings() {
@@ -60958,14 +62330,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
60958
62330
  };
60959
62331
  //#endregion
60960
62332
  //#region src/features/permissions/crush-permissions.ts
60961
- const CATCH_ALL_PATTERN$9 = "*";
62333
+ const CATCH_ALL_PATTERN$10 = "*";
60962
62334
  /**
60963
62335
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
60964
62336
  * argument patterns at all, so it is the only pattern a rule can carry and
60965
62337
  * still be written.
60966
62338
  */
60967
62339
  function normalizeCatchAllPattern(pattern) {
60968
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
62340
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
60969
62341
  }
60970
62342
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
60971
62343
  bash: "bash",
@@ -61164,7 +62536,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
61164
62536
  for (const [rawPattern, action] of Object.entries(rules)) {
61165
62537
  if (isPrototypePollutionKey(rawPattern)) continue;
61166
62538
  const pattern = normalizeCatchAllPattern(rawPattern);
61167
- if (pattern === CATCH_ALL_PATTERN$9) {
62539
+ if (pattern === CATCH_ALL_PATTERN$10) {
61168
62540
  catchAllAction = action;
61169
62541
  continue;
61170
62542
  }
@@ -61237,11 +62609,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
61237
62609
  };
61238
62610
  for (const entry of disabled) {
61239
62611
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
61240
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] = "deny";
62612
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
61241
62613
  }
61242
62614
  for (const entry of allowed) {
61243
62615
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
61244
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$9] ??= "allow";
62616
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
61245
62617
  }
61246
62618
  return { permission };
61247
62619
  }
@@ -62683,7 +64055,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
62683
64055
  * (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
62684
64056
  * merge into the existing JSON and the file is never deleted; only the keys
62685
64057
  * this feature manages are rewritten — `permissions`, plus `sandbox` in global
62686
- * mode when the `devin` override authors it.
64058
+ * mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
64059
+ * `forbiddenPlugins`) in project mode when the `devin` override authors them.
62687
64060
  *
62688
64061
  * The sibling `sandbox` block — which decides what a permitted command may
62689
64062
  * reach rather than which commands are permitted — has no canonical category
@@ -62704,7 +64077,8 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62704
64077
  /**
62705
64078
  * config.json may carry the MCP/hooks features' keys, so it is never deleted;
62706
64079
  * only the keys this feature manages are rewritten — `permissions`, plus
62707
- * `sandbox` in global mode when the `devin` override authors it.
64080
+ * `sandbox` (global) and the plugin governance lists (project) when the
64081
+ * `devin` override authors them.
62708
64082
  */
62709
64083
  isDeletable() {
62710
64084
  return false;
@@ -62799,6 +64173,13 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62799
64173
  });
62800
64174
  } else if (Object.keys(authoredSandboxRecord).length > 0) logger?.warn("Devin reads 'sandbox' from the user config only, so the 'devin.sandbox' override was dropped from the project config. Generate with --global to author it.");
62801
64175
  }
64176
+ applyPluginGovernanceOverride({
64177
+ override: config.devin,
64178
+ settings,
64179
+ patch,
64180
+ global,
64181
+ logger
64182
+ });
62802
64183
  return new DevinPermissions({
62803
64184
  outputRoot,
62804
64185
  relativeDirPath: paths.relativeDirPath,
@@ -62813,7 +64194,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62813
64194
  validate
62814
64195
  });
62815
64196
  }
62816
- toRulesyncPermissions() {
64197
+ toRulesyncPermissions({ logger } = {}) {
62817
64198
  let settings;
62818
64199
  try {
62819
64200
  const parsed = JSON.parse(this.getFileContent());
@@ -62828,8 +64209,16 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62828
64209
  deny: Array.isArray(permissions.deny) ? permissions.deny : []
62829
64210
  });
62830
64211
  const sandbox = asDevinRecord(settings.sandbox);
64212
+ const devinOverride = {};
64213
+ if (Object.keys(sandbox).length > 0) devinOverride.sandbox = sandbox;
64214
+ for (const key of DEVIN_PLUGIN_GOVERNANCE_KEYS) {
64215
+ const value = settings[key];
64216
+ if (value === void 0) continue;
64217
+ if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
64218
+ else logger?.warn(`Skipped Devin \`${key}\` on import: ${key === "forbiddenPlugins" ? "expected an array of strings" : "expected an array of plugin sources (strings or objects)"}.`);
64219
+ }
62831
64220
  const result = { ...config };
62832
- if (Object.keys(sandbox).length > 0) result.devin = { sandbox };
64221
+ if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
62833
64222
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
62834
64223
  }
62835
64224
  validate() {
@@ -62849,6 +64238,42 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
62849
64238
  }
62850
64239
  };
62851
64240
  /**
64241
+ * The repo-level plugin governance lists of a checkout's `.devin/config.json`.
64242
+ *
64243
+ * @see https://docs.devin.ai/cli/extensibility/plugins/overview
64244
+ */
64245
+ const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
64246
+ "requiredPlugins",
64247
+ "optionalPlugins",
64248
+ "forbiddenPlugins"
64249
+ ];
64250
+ /**
64251
+ * Patch the plugin governance lists the `devin` override authors into the
64252
+ * project `.devin/config.json`. Each authored list replaces the file's list
64253
+ * whole; a list it does not author is left as the file holds it. Devin reads these
64254
+ * lists only from a checkout's `.devin/config.json`, so in global mode they are
64255
+ * dropped with a warning. Losing a `forbiddenPlugins` entry the file held is
64256
+ * announced, since that lifts a restriction. A non-empty `requiredPlugins` that changes the file
64257
+ * is announced, since Devin installs required plugins automatically and a
64258
+ * plugin can ship its own hooks and MCP servers.
64259
+ */
64260
+ function applyPluginGovernanceOverride({ override, settings, patch, global, logger }) {
64261
+ const authoredKeys = DEVIN_PLUGIN_GOVERNANCE_KEYS.filter((key) => override?.[key] !== void 0);
64262
+ if (authoredKeys.length === 0) return;
64263
+ if (global) {
64264
+ logger?.warn(`Devin reads the plugin governance lists (${authoredKeys.join(", ")}) from a checkout's '.devin/config.json' only, so they were dropped from the user config. Generate without --global to author them.`);
64265
+ return;
64266
+ }
64267
+ for (const key of authoredKeys) patch[key] = override?.[key];
64268
+ const forbidden = override?.forbiddenPlugins;
64269
+ if (Array.isArray(forbidden) && Array.isArray(settings.forbiddenPlugins)) {
64270
+ const lost = settings.forbiddenPlugins.filter((entry) => !forbidden.includes(entry));
64271
+ if (lost.length > 0) logger?.warn(`Replacing Devin \`forbiddenPlugins\` drops ${lost.map(quoteValueForWarning).join(", ")} from '.devin/config.json': those plugins are no longer forbidden at the repo level.`);
64272
+ }
64273
+ const required = override?.requiredPlugins;
64274
+ if (Array.isArray(required) && required.length > 0 && !isDeepStrictEqual(required, settings.requiredPlugins)) logger?.warn(`Writing Devin \`requiredPlugins\` (${required.map(quoteValueForWarning).join(", ")}): Devin installs required plugins automatically, and a plugin can ship its own hooks and MCP servers.`);
64275
+ }
64276
+ /**
62852
64277
  * Collapse the pattern rules of a bare-only scope to the single action Devin
62853
64278
  * can hold for it, using deny > ask > allow precedence. A map without a
62854
64279
  * catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
@@ -63106,7 +64531,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
63106
64531
  //#region src/features/permissions/goose-permissions.ts
63107
64532
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
63108
64533
  const GOOSE_USER_KEY = "user";
63109
- const CATCH_ALL_PATTERN$8 = "*";
64534
+ const CATCH_ALL_PATTERN$9 = "*";
63110
64535
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
63111
64536
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
63112
64537
  shell: ["bash"],
@@ -63118,7 +64543,7 @@ const ACTION_TO_GOOSE_LIST = {
63118
64543
  ask: "ask_before",
63119
64544
  deny: "never_allow"
63120
64545
  };
63121
- const ACTION_RANK$4 = {
64546
+ const ACTION_RANK$5 = {
63122
64547
  allow: 0,
63123
64548
  ask: 1,
63124
64549
  deny: 2
@@ -63270,14 +64695,14 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
63270
64695
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
63271
64696
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
63272
64697
  for (const [pattern, action] of Object.entries(rules)) {
63273
- if (pattern !== CATCH_ALL_PATTERN$8) {
64698
+ if (pattern !== CATCH_ALL_PATTERN$9) {
63274
64699
  logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
63275
64700
  continue;
63276
64701
  }
63277
64702
  const previous = assigned.get(toolName);
63278
64703
  if (previous === action) continue;
63279
64704
  if (previous !== void 0) {
63280
- const stricter = ACTION_RANK$4[action] > ACTION_RANK$4[previous] ? action : previous;
64705
+ const stricter = ACTION_RANK$5[action] > ACTION_RANK$5[previous] ? action : previous;
63281
64706
  logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
63282
64707
  if (stricter === previous) continue;
63283
64708
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
@@ -63301,7 +64726,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
63301
64726
  const action = GOOSE_LIST_TO_ACTION[key];
63302
64727
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
63303
64728
  permission[category] ??= {};
63304
- permission[category][CATCH_ALL_PATTERN$8] = action;
64729
+ permission[category][CATCH_ALL_PATTERN$9] = action;
63305
64730
  }
63306
64731
  }
63307
64732
  return { permission };
@@ -63312,7 +64737,7 @@ const GROKCLI_UI_KEY = "ui";
63312
64737
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
63313
64738
  const GROKCLI_PERMISSION_KEY = "permission";
63314
64739
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
63315
- const CATCH_ALL_PATTERN$7 = "*";
64740
+ const CATCH_ALL_PATTERN$8 = "*";
63316
64741
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
63317
64742
  const CATEGORY_TO_GROK_TOOL = {
63318
64743
  bash: "Bash",
@@ -63358,15 +64783,15 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
63358
64783
  * under it has no Grok spelling and returns `null`.
63359
64784
  */
63360
64785
  function buildGrokEntry(category, pattern) {
63361
- if (category === "*") return pattern === CATCH_ALL_PATTERN$7 ? GROK_ALL_TOOLS_ENTRY : null;
64786
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
63362
64787
  if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
63363
64788
  const remainder = category.slice(5);
63364
64789
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
63365
64790
  }
63366
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
64791
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
63367
64792
  const tool = CATEGORY_TO_GROK_TOOL[category];
63368
64793
  if (tool === void 0) return null;
63369
- return pattern === CATCH_ALL_PATTERN$7 || pattern === "" ? tool : `${tool}(${pattern})`;
64794
+ return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
63370
64795
  }
63371
64796
  /**
63372
64797
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -63388,21 +64813,21 @@ function parseGrokEntry(entry) {
63388
64813
  }
63389
64814
  if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
63390
64815
  category: "*",
63391
- pattern: CATCH_ALL_PATTERN$7
64816
+ pattern: CATCH_ALL_PATTERN$8
63392
64817
  };
63393
64818
  if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$3)) return parseClaudeStyleMcpEntry(tool);
63394
64819
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
63395
64820
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
63396
- pattern: CATCH_ALL_PATTERN$7
64821
+ pattern: CATCH_ALL_PATTERN$8
63397
64822
  } : {
63398
64823
  category: "mcp",
63399
- pattern: CATCH_ALL_PATTERN$7
64824
+ pattern: CATCH_ALL_PATTERN$8
63400
64825
  };
63401
64826
  const category = GROK_TOOL_TO_CATEGORY[tool];
63402
64827
  if (category === void 0) return null;
63403
64828
  return {
63404
64829
  category,
63405
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$7
64830
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
63406
64831
  };
63407
64832
  }
63408
64833
  /**
@@ -63419,14 +64844,14 @@ function parseGrokEntry(entry) {
63419
64844
  function parseClaudeStyleMcpEntry(entry) {
63420
64845
  const rest = entry.slice(5);
63421
64846
  if (rest.length === 0) return null;
63422
- if (rest === CATCH_ALL_PATTERN$7) return {
64847
+ if (rest === CATCH_ALL_PATTERN$8) return {
63423
64848
  category: "mcp",
63424
- pattern: CATCH_ALL_PATTERN$7
64849
+ pattern: CATCH_ALL_PATTERN$8
63425
64850
  };
63426
64851
  const address = rest.includes("__") ? rest : `${rest}__*`;
63427
64852
  return {
63428
64853
  category: `${MCP_CANONICAL_PREFIX$3}${address}`,
63429
- pattern: CATCH_ALL_PATTERN$7
64854
+ pattern: CATCH_ALL_PATTERN$8
63430
64855
  };
63431
64856
  }
63432
64857
  /**
@@ -63447,20 +64872,20 @@ function parseGrokRule(rule) {
63447
64872
  if (typeof tool !== "string") return null;
63448
64873
  if (pattern !== void 0 && typeof pattern !== "string") return null;
63449
64874
  const trimmedPattern = pattern?.trim() ?? "";
63450
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$7;
64875
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
63451
64876
  const lowerTool = tool.trim().toLowerCase();
63452
- if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
64877
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
63453
64878
  category: "*",
63454
- pattern: CATCH_ALL_PATTERN$7,
64879
+ pattern: CATCH_ALL_PATTERN$8,
63455
64880
  action
63456
64881
  } : null;
63457
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$7 ? {
64882
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
63458
64883
  category: "mcp",
63459
- pattern: CATCH_ALL_PATTERN$7,
64884
+ pattern: CATCH_ALL_PATTERN$8,
63460
64885
  action
63461
64886
  } : {
63462
64887
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
63463
- pattern: CATCH_ALL_PATTERN$7,
64888
+ pattern: CATCH_ALL_PATTERN$8,
63464
64889
  action
63465
64890
  };
63466
64891
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -63613,7 +65038,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
63613
65038
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
63614
65039
  }
63615
65040
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
63616
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$7]: legacyModeAction(parsed) } } };
65041
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$8]: legacyModeAction(parsed) } } };
63617
65042
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
63618
65043
  }
63619
65044
  validate() {
@@ -63633,7 +65058,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
63633
65058
  });
63634
65059
  }
63635
65060
  };
63636
- const ACTION_RANK$3 = {
65061
+ const ACTION_RANK$4 = {
63637
65062
  allow: 0,
63638
65063
  ask: 1,
63639
65064
  deny: 2
@@ -63668,7 +65093,7 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
63668
65093
  }
63669
65094
  const existing = ranked.get(entry);
63670
65095
  if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
63671
- if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) ranked.set(entry, action);
65096
+ if (existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing]) ranked.set(entry, action);
63672
65097
  }
63673
65098
  warnRemovedAllToolsRestrictions({
63674
65099
  existingPermission,
@@ -63704,7 +65129,7 @@ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger })
63704
65129
  const entries = existingPermission[action];
63705
65130
  if (!isStringArray$1(entries)) continue;
63706
65131
  if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63707
- if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
65132
+ if (generated !== void 0 && ACTION_RANK$4[generated] >= ACTION_RANK$4[action]) continue;
63708
65133
  logger.warn(`Grok CLI permissions: removing the existing bare '*' entry from the '${action}' array — Rulesync now manages it through the all-tools '*' category, and the canonical config has no '*' '${action}' rule. Add "*": { "*": "${action}" } to keep restricting every tool.`);
63709
65134
  }
63710
65135
  }
@@ -63719,7 +65144,7 @@ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63719
65144
  if (!logger) return;
63720
65145
  const allTools = ranked.get(GROK_ALL_TOOLS_ENTRY);
63721
65146
  if (allTools === void 0 || allTools === "allow") return;
63722
- const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$3[action] < ACTION_RANK$3[allTools]).map(([entry]) => entry).toSorted();
65147
+ const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$4[action] < ACTION_RANK$4[allTools]).map(([entry]) => entry).toSorted();
63723
65148
  if (shadowed.length === 0) return;
63724
65149
  logger.warn(`Grok CLI permissions: the bare '*' '${allTools}' rule applies to every tool, and Grok evaluates deny > ask > allow, so these less strict entries never take effect: ${shadowed.join(", ")}.`);
63725
65150
  }
@@ -63742,7 +65167,7 @@ function parseGrokPermissionArrays(permission) {
63742
65167
  const record = ({ category, pattern, action }) => {
63743
65168
  const bucket = result[category] ??= {};
63744
65169
  const existing = bucket[pattern];
63745
- if (existing === void 0 || ACTION_RANK$3[action] > ACTION_RANK$3[existing]) bucket[pattern] = action;
65170
+ if (existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing]) bucket[pattern] = action;
63746
65171
  };
63747
65172
  const applyEntries = (entries, action) => {
63748
65173
  for (const entry of entries ?? []) {
@@ -64146,7 +65571,7 @@ const JUNIE_GROUP_TO_CANONICAL = {
64146
65571
  mcpTools: "mcp",
64147
65572
  readOutsideProject: "read"
64148
65573
  };
64149
- function isPermissionAction$2(value) {
65574
+ function isPermissionAction$3(value) {
64150
65575
  return PermissionActionSchema.safeParse(value).success;
64151
65576
  }
64152
65577
  /**
@@ -64332,7 +65757,7 @@ function convertJunieToRulesyncPermissions({ allowlist }) {
64332
65757
  for (const rule of Array.isArray(ruleSet.rules) ? ruleSet.rules : []) {
64333
65758
  if (!rule || typeof rule !== "object") continue;
64334
65759
  const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
64335
- if (pattern === void 0 || !isPermissionAction$2(rule.action)) continue;
65760
+ if (pattern === void 0 || !isPermissionAction$3(rule.action)) continue;
64336
65761
  (permission[category] ??= {})[pattern] = rule.action;
64337
65762
  }
64338
65763
  }
@@ -65292,7 +66717,7 @@ function rulesFromArrays(settings, allowKey, denyKey) {
65292
66717
  }
65293
66718
  //#endregion
65294
66719
  //#region src/features/permissions/kiro-capability-permissions.ts
65295
- const CATCH_ALL_PATTERN$6 = "*";
66720
+ const CATCH_ALL_PATTERN$7 = "*";
65296
66721
  const CANONICAL_TO_KIRO_CAPABILITY = {
65297
66722
  bash: "shell",
65298
66723
  read: "fs_read",
@@ -65309,12 +66734,12 @@ const KIRO_CAPABILITY_TO_CANONICAL = {
65309
66734
  web_search: "websearch"
65310
66735
  };
65311
66736
  const MANAGED_CAPABILITIES = new Set(Object.keys(KIRO_CAPABILITY_TO_CANONICAL));
65312
- const ACTION_RANK$2 = {
66737
+ const ACTION_RANK$3 = {
65313
66738
  allow: 0,
65314
66739
  ask: 1,
65315
66740
  deny: 2
65316
66741
  };
65317
- function isPermissionAction$1(value) {
66742
+ function isPermissionAction$2(value) {
65318
66743
  return value === "allow" || value === "ask" || value === "deny";
65319
66744
  }
65320
66745
  /**
@@ -65341,7 +66766,7 @@ function rulePatternKey(capability, pattern) {
65341
66766
  function classifyExistingEntry({ emitted, capability, pattern, effect }) {
65342
66767
  const effects = emitted.get(rulePatternKey(capability, pattern));
65343
66768
  if (effects === void 0) return "unrelated";
65344
- return isPermissionAction$1(effect) && effects.has(effect) ? "duplicate" : "conflict";
66769
+ return isPermissionAction$2(effect) && effects.has(effect) ? "duplicate" : "conflict";
65345
66770
  }
65346
66771
  /**
65347
66772
  * Merge one existing rule against the generated entries (see
@@ -65356,7 +66781,7 @@ function mergeExistingRule({ rule, emitted, logger }) {
65356
66781
  };
65357
66782
  const capability = rule.capability;
65358
66783
  const isCatchAll = rule.match === void 0;
65359
- const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
66784
+ const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$7];
65360
66785
  if (patterns.length === 0) return {
65361
66786
  rule,
65362
66787
  unrelatedEntries: 0
@@ -65410,7 +66835,7 @@ function mergeExistingRule({ rule, emitted, logger }) {
65410
66835
  */
65411
66836
  function mergeKiroCapabilityRules({ existingRules, generatedRules, logger }) {
65412
66837
  const emitted = /* @__PURE__ */ new Map();
65413
- for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$6]) {
66838
+ for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$7]) {
65414
66839
  const key = rulePatternKey(rule.capability, pattern);
65415
66840
  const effects = emitted.get(key) ?? /* @__PURE__ */ new Set();
65416
66841
  effects.add(rule.effect);
@@ -65538,13 +66963,13 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
65538
66963
  const rules = isRecord$1(parsed) && Array.isArray(parsed.rules) ? parsed.rules : [];
65539
66964
  const permission = {};
65540
66965
  for (const rule of rules) {
65541
- if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
66966
+ if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$2(rule.effect)) continue;
65542
66967
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
65543
- const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$1(rule.match) ? rule.match : [];
66968
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$7] : isStringArray$1(rule.match) ? rule.match : [];
65544
66969
  const categoryRules = permission[category] ??= {};
65545
66970
  for (const pattern of patterns) {
65546
66971
  const previous = categoryRules[pattern];
65547
- if (previous === void 0 || ACTION_RANK$2[rule.effect] > ACTION_RANK$2[previous]) categoryRules[pattern] = rule.effect;
66972
+ if (previous === void 0 || ACTION_RANK$3[rule.effect] > ACTION_RANK$3[previous]) categoryRules[pattern] = rule.effect;
65548
66973
  }
65549
66974
  }
65550
66975
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
@@ -65598,11 +67023,11 @@ function buildKiroCapabilityRules({ config, logger }) {
65598
67023
  }
65599
67024
  const result = [];
65600
67025
  for (const { capability, effect, patterns } of buckets.values()) {
65601
- if (patterns.has(CATCH_ALL_PATTERN$6)) result.push({
67026
+ if (patterns.has(CATCH_ALL_PATTERN$7)) result.push({
65602
67027
  capability,
65603
67028
  effect
65604
67029
  });
65605
- const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$6);
67030
+ const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$7);
65606
67031
  if (match.length > 0) result.push({
65607
67032
  capability,
65608
67033
  match,
@@ -65627,7 +67052,7 @@ function serializeKiroPermissions({ next, parsed, existingContent }) {
65627
67052
  */
65628
67053
  function warnOnWorkspaceRelativeFsPattern({ capability, category, pattern, logger }) {
65629
67054
  if (capability !== "fs_read" && capability !== "fs_write") return;
65630
- if (pattern === CATCH_ALL_PATTERN$6 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
67055
+ if (pattern === CATCH_ALL_PATTERN$7 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
65631
67056
  logger?.warn(`Kiro permissions.yaml is user-level and applies to every workspace, but the ${category} pattern "${pattern}" is relative. Use an absolute, ~-relative or **-prefixed pattern to target a specific location.`);
65632
67057
  }
65633
67058
  /**
@@ -65655,7 +67080,7 @@ const KiroScopedPermissions = {
65655
67080
  //#region src/features/permissions/lettacode-permissions.ts
65656
67081
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
65657
67082
  const LETTACODE_PERMISSIONS_KEY = "permissions";
65658
- const CATCH_ALL_PATTERN$5 = "*";
67083
+ const CATCH_ALL_PATTERN$6 = "*";
65659
67084
  /** Letta Code's `Bash(prefix:*)` prefix-match suffix. */
65660
67085
  const PREFIX_SUFFIX = ":*";
65661
67086
  const CATEGORY_TO_LETTACODE_TOOL = {
@@ -65700,12 +67125,12 @@ const LIST_TO_ACTION = {
65700
67125
  alwaysAsk: "ask",
65701
67126
  deny: "deny"
65702
67127
  };
65703
- const ACTION_RANK$1 = {
67128
+ const ACTION_RANK$2 = {
65704
67129
  allow: 0,
65705
67130
  ask: 1,
65706
67131
  deny: 2
65707
67132
  };
65708
- function toStringList$1(value) {
67133
+ function toStringList$2(value) {
65709
67134
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65710
67135
  }
65711
67136
  /**
@@ -65748,7 +67173,7 @@ function categoryOfRule$1(rule) {
65748
67173
  * widening it would grant commands the source never allowed.
65749
67174
  */
65750
67175
  function toLettacodeBashPayload({ pattern, action, logger }) {
65751
- if (pattern === CATCH_ALL_PATTERN$5) return "";
67176
+ if (pattern === CATCH_ALL_PATTERN$6) return "";
65752
67177
  const firstStar = pattern.indexOf("*");
65753
67178
  if (firstStar === -1) return pattern;
65754
67179
  if (pattern.endsWith(PREFIX_SUFFIX) && firstStar === pattern.length - 1) return pattern;
@@ -65769,7 +67194,7 @@ function toLettacodeRule({ category, pattern, action, logger }) {
65769
67194
  pattern,
65770
67195
  action,
65771
67196
  logger
65772
- }) : pattern === CATCH_ALL_PATTERN$5 ? "" : pattern;
67197
+ }) : pattern === CATCH_ALL_PATTERN$6 ? "" : pattern;
65773
67198
  if (payload === null) return null;
65774
67199
  return payload === "" ? tool : `${tool}(${payload})`;
65775
67200
  }
@@ -65780,7 +67205,7 @@ function fromLettacodeRule(rule) {
65780
67205
  const { payload } = splitLettacodeRule(rule);
65781
67206
  if (payload === void 0) return {
65782
67207
  category,
65783
- pattern: CATCH_ALL_PATTERN$5
67208
+ pattern: CATCH_ALL_PATTERN$6
65784
67209
  };
65785
67210
  const trimmed = payload.trim();
65786
67211
  if (trimmed === "") return null;
@@ -65788,7 +67213,7 @@ function fromLettacodeRule(rule) {
65788
67213
  const prefix = trimmed.slice(0, -2).trimEnd();
65789
67214
  return {
65790
67215
  category,
65791
- pattern: prefix === "" ? CATCH_ALL_PATTERN$5 : `${prefix} *`
67216
+ pattern: prefix === "" ? CATCH_ALL_PATTERN$6 : `${prefix} *`
65792
67217
  };
65793
67218
  }
65794
67219
  return {
@@ -65823,11 +67248,11 @@ function buildLettacodeRuleLists({ config, existingPermissions, logger }) {
65823
67248
  });
65824
67249
  if (rule === null) continue;
65825
67250
  const existing = ranked.get(rule);
65826
- if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) ranked.set(rule, action);
67251
+ if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) ranked.set(rule, action);
65827
67252
  }
65828
67253
  }
65829
67254
  const lists = {};
65830
- for (const key of Object.keys(LIST_TO_ACTION)) lists[key] = toStringList$1(existingPermissions[key]).filter((rule) => {
67255
+ for (const key of Object.keys(LIST_TO_ACTION)) lists[key] = toStringList$2(existingPermissions[key]).filter((rule) => {
65831
67256
  const category = categoryOfRule$1(rule);
65832
67257
  return category === void 0 || !managedCategories.has(category);
65833
67258
  });
@@ -65916,12 +67341,12 @@ var LettacodePermissions = class LettacodePermissions extends ToolPermissions {
65916
67341
  });
65917
67342
  const permissions = isRecord$1(settings[LETTACODE_PERMISSIONS_KEY]) ? settings[LETTACODE_PERMISSIONS_KEY] : {};
65918
67343
  const permission = {};
65919
- for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringList$1(permissions[key])) {
67344
+ for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringList$2(permissions[key])) {
65920
67345
  const parsed = fromLettacodeRule(rule);
65921
67346
  if (parsed === null || isPrototypePollutionKey(parsed.pattern)) continue;
65922
67347
  const rules = permission[parsed.category] ??= {};
65923
67348
  const existing = rules[parsed.pattern];
65924
- if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) rules[parsed.pattern] = action;
67349
+ if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) rules[parsed.pattern] = action;
65925
67350
  }
65926
67351
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
65927
67352
  }
@@ -66105,7 +67530,11 @@ var OpencodePermissions = class extends ToolPermissions {
66105
67530
  fileContent = await readFileContentOrNull(jsonPath);
66106
67531
  if (fileContent) relativeFilePath = this.layout.jsonFileName;
66107
67532
  }
66108
- const parsed = parse$1(fileContent ?? "{}");
67533
+ const parsed = (await readOpencodeDotDirConfigLayers({
67534
+ outputRoot,
67535
+ global,
67536
+ layout: this.layout
67537
+ })).map(({ config }) => config).reduce(mergeOpencodeConfigValues, parse$1(fileContent ?? "{}"));
66109
67538
  const record = isRecord$1(parsed) ? parsed : {};
66110
67539
  const nextJson = {
66111
67540
  ...record,
@@ -66156,6 +67585,16 @@ var OpencodePermissions = class extends ToolPermissions {
66156
67585
  value,
66157
67586
  logger
66158
67587
  });
67588
+ warnOpencodeDotDirOverrides({
67589
+ generated: { permission },
67590
+ overlays: await readOpencodeDotDirConfigLayers({
67591
+ outputRoot,
67592
+ global,
67593
+ layout: this.layout
67594
+ }),
67595
+ rootFileName: relativeFilePath,
67596
+ logger
67597
+ });
66159
67598
  return new this({
66160
67599
  outputRoot,
66161
67600
  relativeDirPath: basePaths.relativeDirPath,
@@ -66245,7 +67684,7 @@ var MimocodePermissions = class extends OpencodePermissions {
66245
67684
  };
66246
67685
  //#endregion
66247
67686
  //#region src/features/permissions/omp-permissions.ts
66248
- const CATCH_ALL_PATTERN$4 = "*";
67687
+ const CATCH_ALL_PATTERN$5 = "*";
66249
67688
  const APPROVAL_MODE_KEY = "approvalMode";
66250
67689
  const ACTION_TO_OMP_APPROVAL = {
66251
67690
  allow: "allow",
@@ -66442,7 +67881,7 @@ function allToolsFloor(rules) {
66442
67881
  let patternSpecific = false;
66443
67882
  for (const [pattern, action] of Object.entries(rules ?? {})) {
66444
67883
  if (action === "allow" || isPrototypePollutionKey(pattern)) continue;
66445
- if (pattern === CATCH_ALL_PATTERN$4) floor = stricter(floor, action);
67884
+ if (pattern === CATCH_ALL_PATTERN$5) floor = stricter(floor, action);
66446
67885
  else {
66447
67886
  patternSpecific = true;
66448
67887
  floor = stricter(floor, "ask");
@@ -66477,11 +67916,11 @@ function convertBashRules$1({ rules, logger }) {
66477
67916
  const entries = Object.entries(rules).filter(([pattern]) => !isPrototypePollutionKey(pattern));
66478
67917
  const hasAsk = entries.some(([, action]) => action === "ask");
66479
67918
  let policy = hasAsk ? "ask" : void 0;
66480
- if (hasAsk && rules[CATCH_ALL_PATTERN$4] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
67919
+ if (hasAsk && rules[CATCH_ALL_PATTERN$5] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
66481
67920
  for (const [pattern, action] of entries) {
66482
- if (pattern === CATCH_ALL_PATTERN$4 && action === "allow") {
67921
+ if (pattern === CATCH_ALL_PATTERN$5 && action === "allow") {
66483
67922
  if (hasAsk) patterns.push({
66484
- match: CATCH_ALL_PATTERN$4,
67923
+ match: CATCH_ALL_PATTERN$5,
66485
67924
  approval: "allow"
66486
67925
  });
66487
67926
  else policy = "allow";
@@ -66584,12 +68023,12 @@ function toOmpApproval(value) {
66584
68023
  * ask rules (see `convertBashRules`), so it adds no catch-all.
66585
68024
  */
66586
68025
  function importBashPolicy(bashRules, action) {
66587
- const catchAll = bashRules[CATCH_ALL_PATTERN$4];
68026
+ const catchAll = bashRules[CATCH_ALL_PATTERN$5];
66588
68027
  const hasAskPattern = Object.values(bashRules).includes("ask");
66589
- if (action === "deny") bashRules[CATCH_ALL_PATTERN$4] = "deny";
68028
+ if (action === "deny") bashRules[CATCH_ALL_PATTERN$5] = "deny";
66590
68029
  else if (catchAll === void 0) {
66591
- if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = action;
66592
- } else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = "ask";
68030
+ if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$5] = action;
68031
+ } else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$5] = "ask";
66593
68032
  }
66594
68033
  /** `bash.patterns` as canonical `bash` rules, in first-match order. */
66595
68034
  function importBashPatterns(value) {
@@ -66617,7 +68056,7 @@ function convertOmpToRulesync(config) {
66617
68056
  const action = OMP_APPROVAL_TO_ACTION[approval];
66618
68057
  if (tool === "bash") continue;
66619
68058
  const category = renamed(OMP_TOOL_TO_CATEGORY, tool);
66620
- permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
68059
+ permission[category] = { [CATCH_ALL_PATTERN$5]: stricter(permission[category]?.[CATCH_ALL_PATTERN$5], action) };
66621
68060
  }
66622
68061
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
66623
68062
  const approvalMode = tools[APPROVAL_MODE_KEY];
@@ -66747,7 +68186,7 @@ const POOL_DENY_KEY = "deny";
66747
68186
  const POOL_DISABLED_KEY = "disabled";
66748
68187
  const POOL_PATH_KEY = "path";
66749
68188
  const POOL_WRITE_KEY = "write";
66750
- const CATCH_ALL_PATTERN$3 = "*";
68189
+ const CATCH_ALL_PATTERN$4 = "*";
66751
68190
  const PATHS_CATCH_ALL_PATTERN = "**";
66752
68191
  const CANONICAL_TO_POOL_TOOL_NAMES = {
66753
68192
  bash: "shell",
@@ -67255,7 +68694,7 @@ function pathOfEntry(entry) {
67255
68694
  */
67256
68695
  function importPoolTool({ toolConfig, bucket }) {
67257
68696
  if (toolConfig[POOL_DISABLED_KEY] === true) {
67258
- bucket[CATCH_ALL_PATTERN$3] = "deny";
68697
+ bucket[CATCH_ALL_PATTERN$4] = "deny";
67259
68698
  return;
67260
68699
  }
67261
68700
  const denied = isStringArray$1(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
@@ -67312,9 +68751,58 @@ const QODER_PERMISSIONS_KEY = "permissions";
67312
68751
  * @see https://docs.qoder.com/cli/settings-reference
67313
68752
  */
67314
68753
  const QODER_OUTPUT_STYLE_KEY = "outputStyle";
67315
- const CATCH_ALL_PATTERN$2 = "*";
68754
+ /**
68755
+ * The settings groups the `qoder` override may author, each with the only keys
68756
+ * of that group rulesync writes and imports. A group's other keys are kept as
68757
+ * the file has them; an override key not listed here is skipped with a
68758
+ * warning, so a shared permissions file cannot reach `tools.shell.pager`,
68759
+ * `general.preferredEditor` and the like.
68760
+ * @see https://docs.qoder.com/cli/settings-reference
68761
+ */
68762
+ const QODER_OVERRIDE_GROUP_KEYS = {
68763
+ general: ["defaultPermissionMode"],
68764
+ tools: ["core", "exclude"],
68765
+ mcp: [
68766
+ "allowed",
68767
+ "excluded",
68768
+ "enableAllProjectMcpServers",
68769
+ "enabledProjectMcpServers",
68770
+ "lazyLoad"
68771
+ ],
68772
+ agents: ["overrides"]
68773
+ };
68774
+ const QODER_OVERRIDE_GROUPS = Object.keys(QODER_OVERRIDE_GROUP_KEYS);
68775
+ /**
68776
+ * Top-level Auto-mode classifier guidance. Qoder reads it from the user
68777
+ * settings only (a workspace file could otherwise bias the classifier), so it
68778
+ * is written and imported in global mode alone.
68779
+ * @see https://docs.qoder.com/en/cli/permissions
68780
+ */
68781
+ const QODER_AUTO_MODE_KEY = "autoMode";
68782
+ /** The `autoMode` keys rulesync writes and imports. */
68783
+ const QODER_AUTO_MODE_KEYS = [
68784
+ "allow",
68785
+ "soft_deny",
68786
+ "environment"
68787
+ ];
68788
+ /** Top-level globs that keep project/local memory files from loading. */
68789
+ const QODER_AGENTS_MD_EXCLUDES_KEY = "agentsMdExcludes";
68790
+ /**
68791
+ * `general.defaultPermissionMode` values (lower-cased; Qoder matches them
68792
+ * case-insensitively) that let actions run without a prompt.
68793
+ * @see https://docs.qoder.com/en/cli/permissions
68794
+ */
68795
+ const QODER_PERMISSIVE_MODES = /* @__PURE__ */ new Set([
68796
+ "accept_edits",
68797
+ "acceptedits",
68798
+ "auto",
68799
+ "bypass_permissions",
68800
+ "bypasspermissions",
68801
+ "yolo"
68802
+ ]);
68803
+ const CATCH_ALL_PATTERN$3 = "*";
67316
68804
  /** Prefix of the fully qualified MCP tool names (`mcp__<server>__<tool>`). */
67317
- const MCP_TOOL_PREFIX = "mcp__";
68805
+ const MCP_TOOL_PREFIX$1 = "mcp__";
67318
68806
  const CATEGORY_TO_QODER_TOOL = {
67319
68807
  bash: "Bash",
67320
68808
  read: "Read",
@@ -67344,12 +68832,12 @@ const ACTIONS = [
67344
68832
  "ask",
67345
68833
  "deny"
67346
68834
  ];
67347
- const ACTION_RANK = {
68835
+ const ACTION_RANK$1 = {
67348
68836
  allow: 0,
67349
68837
  ask: 1,
67350
68838
  deny: 2
67351
68839
  };
67352
- function toStringList(value) {
68840
+ function toStringList$1(value) {
67353
68841
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
67354
68842
  }
67355
68843
  /**
@@ -67371,7 +68859,7 @@ function splitQoderRule(rule) {
67371
68859
  function categoryOfRule(rule) {
67372
68860
  const { tool } = splitQoderRule(rule);
67373
68861
  if (tool === null) return;
67374
- if (tool === "*" || tool.startsWith(MCP_TOOL_PREFIX)) return tool;
68862
+ if (tool === "*" || tool.startsWith(MCP_TOOL_PREFIX$1)) return tool;
67375
68863
  return Object.hasOwn(QODER_TOOL_TO_CATEGORY, tool) ? QODER_TOOL_TO_CATEGORY[tool] : void 0;
67376
68864
  }
67377
68865
  /**
@@ -67390,18 +68878,18 @@ function categoryOfRule(rule) {
67390
68878
  */
67391
68879
  function toQoderRule({ category, pattern, logger }) {
67392
68880
  if (category === "*") {
67393
- if (pattern === CATCH_ALL_PATTERN$2) return CATCH_ALL_PATTERN$2;
68881
+ if (pattern === CATCH_ALL_PATTERN$3) return CATCH_ALL_PATTERN$3;
67394
68882
  logger?.warn(`Qoder permissions: skipping the all-tools '*' rule '${pattern}' — a Qoder rule with a payload names one tool. Write it under the categories it is meant for (for example 'bash' or 'read').`);
67395
68883
  return null;
67396
68884
  }
67397
- if (category.startsWith(MCP_TOOL_PREFIX)) {
67398
- if (pattern === CATCH_ALL_PATTERN$2) return category;
68885
+ if (category.startsWith(MCP_TOOL_PREFIX$1)) {
68886
+ if (pattern === CATCH_ALL_PATTERN$3) return category;
67399
68887
  logger?.warn(`Qoder permissions: skipping the '${category}' rule '${pattern}' — Qoder matches MCP tools by name only.`);
67400
68888
  return null;
67401
68889
  }
67402
68890
  const tool = CATEGORY_TO_QODER_TOOL[category];
67403
68891
  if (tool === void 0) return null;
67404
- if (pattern === CATCH_ALL_PATTERN$2) return tool;
68892
+ if (pattern === CATCH_ALL_PATTERN$3) return tool;
67405
68893
  return `${QODER_PATH_RULE_ALIASES[tool] ?? tool}(${pattern})`;
67406
68894
  }
67407
68895
  /** Read a Qoder rule back into a canonical category and pattern. */
@@ -67411,9 +68899,9 @@ function fromQoderRule(rule) {
67411
68899
  const { payload } = splitQoderRule(rule);
67412
68900
  if (payload === void 0) return {
67413
68901
  category,
67414
- pattern: CATCH_ALL_PATTERN$2
68902
+ pattern: CATCH_ALL_PATTERN$3
67415
68903
  };
67416
- if (category === "*" || category.startsWith(MCP_TOOL_PREFIX)) return null;
68904
+ if (category === "*" || category.startsWith(MCP_TOOL_PREFIX$1)) return null;
67417
68905
  const trimmed = payload.trim();
67418
68906
  if (trimmed === "") return null;
67419
68907
  return {
@@ -67433,7 +68921,7 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67433
68921
  const ranked = /* @__PURE__ */ new Map();
67434
68922
  const managedCategories = /* @__PURE__ */ new Set();
67435
68923
  for (const [category, rules] of Object.entries(permission)) {
67436
- if (category !== "*" && !category.startsWith(MCP_TOOL_PREFIX) && !Object.hasOwn(CATEGORY_TO_QODER_TOOL, category)) {
68924
+ if (category !== "*" && !category.startsWith(MCP_TOOL_PREFIX$1) && !Object.hasOwn(CATEGORY_TO_QODER_TOOL, category)) {
67437
68925
  logger?.warn(`Qoder permissions: skipping the '${category}' category — Qoder has no permission rule for it.`);
67438
68926
  continue;
67439
68927
  }
@@ -67447,11 +68935,11 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67447
68935
  });
67448
68936
  if (rule === null) continue;
67449
68937
  const existing = ranked.get(rule);
67450
- if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(rule, action);
68938
+ if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) ranked.set(rule, action);
67451
68939
  }
67452
68940
  }
67453
68941
  const lists = {};
67454
- for (const action of ACTIONS) lists[action] = toStringList(existingPermissions[action]).filter((rule) => {
68942
+ for (const action of ACTIONS) lists[action] = toStringList$1(existingPermissions[action]).filter((rule) => {
67455
68943
  if (ranked.has(rule)) return false;
67456
68944
  const category = categoryOfRule(rule);
67457
68945
  return category === void 0 || !managedCategories.has(category);
@@ -67461,6 +68949,130 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67461
68949
  return lists;
67462
68950
  }
67463
68951
  /**
68952
+ * The keys of one authored override group that rulesync writes; any other key
68953
+ * is skipped with a warning.
68954
+ */
68955
+ function pickQoderGroupKeys({ group, allowedKeys, authored, where, logger }) {
68956
+ const picked = {};
68957
+ for (const [key, value] of Object.entries(authored)) {
68958
+ if (isPrototypePollutionKey(key) || value === void 0) continue;
68959
+ if (!allowedKeys.includes(key)) {
68960
+ logger?.warn(`Qoder permissions: skipping qoder.${group}.${key} — Rulesync writes only ${allowedKeys.map((k) => `${group}.${k}`).join(", ")} from the qoder override; set other keys in ${where} by hand.`);
68961
+ continue;
68962
+ }
68963
+ picked[key] = value;
68964
+ }
68965
+ return picked;
68966
+ }
68967
+ /** Name every written override setting that lets Qoder act with fewer prompts. */
68968
+ function announcePermissiveQoderSettings({ patch, where, logger }) {
68969
+ const mode = (isRecord$1(patch.general) ? patch.general : {}).defaultPermissionMode;
68970
+ if (typeof mode === "string" && QODER_PERMISSIVE_MODES.has(mode.trim().toLowerCase())) logger?.warn(`Qoder permissions: qoder.general.defaultPermissionMode = ${JSON.stringify(mode)} was written to ${where}, so Qoder starts every session it applies to in a mode that runs actions without asking first.`);
68971
+ const mcp = isRecord$1(patch.mcp) ? patch.mcp : {};
68972
+ if (mcp.enableAllProjectMcpServers === true) logger?.warn(`Qoder permissions: qoder.mcp.enableAllProjectMcpServers = true was written to ${where}, so Qoder starts every project-level MCP server without asking for approval.`);
68973
+ const enabledServers = toStringList$1(mcp.enabledProjectMcpServers);
68974
+ if (enabledServers.length > 0) logger?.warn(`Qoder permissions: qoder.mcp.enabledProjectMcpServers was written to ${where}, so Qoder starts the project-level MCP servers ${enabledServers.map((name) => JSON.stringify(name)).join(", ")} without asking for approval.`);
68975
+ if (toStringList$1((isRecord$1(patch[QODER_AUTO_MODE_KEY]) ? patch[QODER_AUTO_MODE_KEY] : {}).allow).length > 0) logger?.warn(`Qoder permissions: qoder.autoMode.allow was written to ${where}, so Qoder's Auto-mode classifier leans toward approving the actions it describes.`);
68976
+ }
68977
+ /**
68978
+ * The top-level keys the `qoder` override contributes to the settings patch,
68979
+ * besides `permissions` and `outputStyle`. Each group is the existing group
68980
+ * with the authored keys laid over it; a key is only in the patch when
68981
+ * authored. Every write that lets Qoder act with fewer prompts is announced.
68982
+ */
68983
+ function buildQoderOverridePatch({ override, existing, global, filePath, logger }) {
68984
+ const patch = {};
68985
+ if (override === void 0) return patch;
68986
+ const where = `${filePath}${global ? " in the home directory" : ""}`;
68987
+ for (const group of QODER_OVERRIDE_GROUPS) {
68988
+ const authored = override[group];
68989
+ if (!isRecord$1(authored)) continue;
68990
+ const picked = pickQoderGroupKeys({
68991
+ group,
68992
+ allowedKeys: QODER_OVERRIDE_GROUP_KEYS[group],
68993
+ authored,
68994
+ where,
68995
+ logger
68996
+ });
68997
+ if (Object.keys(picked).length > 0) patch[group] = {
68998
+ ...isRecord$1(existing[group]) ? existing[group] : {},
68999
+ ...picked
69000
+ };
69001
+ }
69002
+ const autoMode = override[QODER_AUTO_MODE_KEY];
69003
+ if (isRecord$1(autoMode)) {
69004
+ if (global) {
69005
+ const picked = pickQoderGroupKeys({
69006
+ group: QODER_AUTO_MODE_KEY,
69007
+ allowedKeys: QODER_AUTO_MODE_KEYS,
69008
+ authored: autoMode,
69009
+ where,
69010
+ logger
69011
+ });
69012
+ if (Object.keys(picked).length > 0) {
69013
+ const existingAutoMode = isRecord$1(existing[QODER_AUTO_MODE_KEY]) ? existing[QODER_AUTO_MODE_KEY] : {};
69014
+ patch[QODER_AUTO_MODE_KEY] = {
69015
+ ...existingAutoMode,
69016
+ ...picked
69017
+ };
69018
+ }
69019
+ } else logger?.warn(`Qoder permissions: skipping qoder.autoMode in project mode — Qoder reads autoMode from the user settings only. Run 'rulesync generate --global' to write it to ~/.qoder/settings.json.`);
69020
+ }
69021
+ const excludes = override[QODER_AGENTS_MD_EXCLUDES_KEY];
69022
+ if (Array.isArray(excludes)) patch[QODER_AGENTS_MD_EXCLUDES_KEY] = toStringList$1(excludes);
69023
+ announcePermissiveQoderSettings({
69024
+ patch: {
69025
+ general: override.general,
69026
+ mcp: override.mcp,
69027
+ ...global && { [QODER_AUTO_MODE_KEY]: autoMode }
69028
+ },
69029
+ where,
69030
+ logger
69031
+ });
69032
+ return patch;
69033
+ }
69034
+ /**
69035
+ * Whether a settings value has the shape the `qoder` override schema accepts
69036
+ * for that key, so an off-spec value is left in the file instead of making the
69037
+ * imported permissions file fail validation.
69038
+ */
69039
+ function isLiftableQoderValue(key, value) {
69040
+ switch (key) {
69041
+ case "defaultPermissionMode": return typeof value === "string" && value.trim() !== "";
69042
+ case "enableAllProjectMcpServers":
69043
+ case "lazyLoad": return typeof value === "boolean";
69044
+ case "overrides": return isRecord$1(value) && Object.values(value).every(isRecord$1);
69045
+ default: return Array.isArray(value) && value.every((entry) => typeof entry === "string");
69046
+ }
69047
+ }
69048
+ /**
69049
+ * Lift the settings the `qoder` override authors (other than `outputStyle`)
69050
+ * back out of a settings file. `autoMode` is lifted only from the user file,
69051
+ * the one Qoder reads it from.
69052
+ */
69053
+ function liftQoderOverride({ settings, global }) {
69054
+ const lifted = {};
69055
+ for (const group of QODER_OVERRIDE_GROUPS) {
69056
+ const existingGroup = settings[group];
69057
+ if (!isRecord$1(existingGroup)) continue;
69058
+ const picked = {};
69059
+ for (const key of QODER_OVERRIDE_GROUP_KEYS[group]) {
69060
+ const value = Object.hasOwn(existingGroup, key) ? existingGroup[key] : void 0;
69061
+ if (isLiftableQoderValue(key, value)) picked[key] = value;
69062
+ }
69063
+ if (Object.keys(picked).length > 0) lifted[group] = picked;
69064
+ }
69065
+ const autoMode = settings[QODER_AUTO_MODE_KEY];
69066
+ if (global && isRecord$1(autoMode)) {
69067
+ const picked = {};
69068
+ for (const key of QODER_AUTO_MODE_KEYS) if (Object.hasOwn(autoMode, key) && isLiftableQoderValue(key, autoMode[key])) picked[key] = autoMode[key];
69069
+ if (Object.keys(picked).length > 0) lifted[QODER_AUTO_MODE_KEY] = picked;
69070
+ }
69071
+ const excludes = settings[QODER_AGENTS_MD_EXCLUDES_KEY];
69072
+ if (isLiftableQoderValue(QODER_AGENTS_MD_EXCLUDES_KEY, excludes)) lifted[QODER_AGENTS_MD_EXCLUDES_KEY] = excludes;
69073
+ return lifted;
69074
+ }
69075
+ /**
67464
69076
  * Qoder permissions.
67465
69077
  *
67466
69078
  * Rules live under the `permissions` key of `.qoder/settings.json` (project)
@@ -67472,7 +69084,9 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
67472
69084
  * (`additionalDirectories`, `trustDirectories`) are kept, and the file is
67473
69085
  * never deleted. `.qoder/settings.local.json` (where `/allow` and `/deny`
67474
69086
  * persist) is left to the user. The `qoder` override's `outputStyle` is
67475
- * written to (and imported from) the top-level `outputStyle` key.
69087
+ * written to (and imported from) the top-level `outputStyle` key, and its
69088
+ * other settings (`general`, `tools`, `mcp`, `agents`, `autoMode`,
69089
+ * `agentsMdExcludes`) through `buildQoderOverridePatch` / `liftQoderOverride`.
67476
69090
  *
67477
69091
  * @see https://docs.qoder.com/en/cli/permissions
67478
69092
  * @see https://docs.qoder.com/cli/output-styles
@@ -67535,7 +69149,14 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
67535
69149
  existingContent,
67536
69150
  patch: {
67537
69151
  [QODER_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
67538
- ...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle }
69152
+ ...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle },
69153
+ ...buildQoderOverridePatch({
69154
+ override: config.qoder,
69155
+ existing,
69156
+ global,
69157
+ filePath: join(paths.relativeDirPath, paths.relativeFilePath),
69158
+ logger
69159
+ })
67539
69160
  },
67540
69161
  filePath
67541
69162
  }),
@@ -67550,18 +69171,24 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
67550
69171
  });
67551
69172
  const permissions = isRecord$1(settings[QODER_PERMISSIONS_KEY]) ? settings[QODER_PERMISSIONS_KEY] : {};
67552
69173
  const permission = {};
67553
- for (const action of ACTIONS) for (const rule of toStringList(permissions[action])) {
69174
+ for (const action of ACTIONS) for (const rule of toStringList$1(permissions[action])) {
67554
69175
  const parsed = fromQoderRule(rule);
67555
69176
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
67556
69177
  const rules = permission[parsed.category] ??= {};
67557
69178
  const existing = rules[parsed.pattern];
67558
- if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[parsed.pattern] = action;
69179
+ if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) rules[parsed.pattern] = action;
67559
69180
  }
67560
69181
  const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
67561
- const hasOutputStyle = typeof outputStyle === "string" && outputStyle.trim() !== "";
69182
+ const qoder = {
69183
+ ...typeof outputStyle === "string" && outputStyle.trim() !== "" && { outputStyle },
69184
+ ...liftQoderOverride({
69185
+ settings,
69186
+ global: this.global
69187
+ })
69188
+ };
67562
69189
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
67563
69190
  permission,
67564
- ...hasOutputStyle && { qoder: { outputStyle } }
69191
+ ...Object.keys(qoder).length > 0 && { qoder }
67565
69192
  }, null, 2) });
67566
69193
  }
67567
69194
  validate() {
@@ -68422,9 +70049,53 @@ const REASONIX_TRUST_AFFECTING_SANDBOX_PATHS = [
68422
70049
  path: ["workspace_root"],
68423
70050
  reason: "moves the root the file-writing tools and sandboxed Bash are confined to, so what they may reach is decided by this path rather than by the project directory",
68424
70051
  widens: escapesTheProject
70052
+ },
70053
+ {
70054
+ path: ["allow_devices"],
70055
+ reason: "binds host device nodes into the Linux Bash sandbox, which otherwise sees a private /dev",
70056
+ widens: isNonEmptyList
70057
+ },
70058
+ {
70059
+ path: ["egress_unlisted"],
70060
+ reason: "lets sandboxed Bash reach hosts that allowed_domains does not name without asking first",
70061
+ widens: (value) => typeof value !== "string" || !["", "ask"].includes(value.trim().toLowerCase())
70062
+ },
70063
+ {
70064
+ path: ["host_authorities"],
70065
+ reason: "lets sandboxed Bash call host services beyond the SSH agent, such as a container daemon socket, which amounts to host-wide execution",
70066
+ widens: (value) => !Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim() !== "ssh_agent")
68425
70067
  }
68426
70068
  ];
68427
70069
  /**
70070
+ * `[sandbox]` keys Reasonix reads only from the user config. In project scope
70071
+ * `narrowSandbox` replaces each of them with the user's value (and reports a
70072
+ * project `egress_unlisted` as user-only), so a repository cannot hand itself a
70073
+ * device, a host service or unprompted egress. A project-scope generate skips
70074
+ * them with a warning, the way `applyPermissionsMode` treats `mode`.
70075
+ *
70076
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.44.0/internal/contract/config/project_scope.go
70077
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.44.0/internal/contract/config/config.go
70078
+ */
70079
+ const REASONIX_USER_ONLY_SANDBOX_KEYS = [
70080
+ "allow_devices",
70081
+ "egress_unlisted",
70082
+ "host_authorities"
70083
+ ];
70084
+ /**
70085
+ * Removes the user-only `[sandbox]` keys from a project-scope override, warning
70086
+ * once about what was skipped. The keys are left alone in the file, so whatever
70087
+ * it already had survives the shallow merge. Global scope keeps them all, and
70088
+ * a non-table override is returned as is, so the trust check still reports its
70089
+ * shape rather than seeing nothing.
70090
+ */
70091
+ function dropUserOnlySandboxKeys({ sandbox, global, relativeFilePath, logger }) {
70092
+ if (global || !isRecord$1(sandbox)) return sandbox;
70093
+ const skipped = REASONIX_USER_ONLY_SANDBOX_KEYS.filter((key) => sandbox[key] !== void 0);
70094
+ if (skipped.length === 0) return sandbox;
70095
+ logger?.warn(`Reasonix permissions: skipping ${skipped.map((key) => `reasonix.sandbox.${key}`).join(", ")} for the project-scoped ${relativeFilePath}; Reasonix reads ${skipped.length === 1 ? "this key" : "these keys"} only from the user config and replaces a project value with the user's, so generate it with --global instead.`);
70096
+ return Object.fromEntries(Object.entries(sandbox).filter(([key]) => !skipped.includes(key)));
70097
+ }
70098
+ /**
68428
70099
  * Whether a `workspace_root` points somewhere other than inside the project the
68429
70100
  * generate runs in. The key moves the write confinement rather than adding to
68430
70101
  * it, so the ordinary value — the project directory, spelled relatively — would
@@ -68661,14 +70332,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
68661
70332
  }));
68662
70333
  const patch = { permissions: mergedPermissions };
68663
70334
  if (override?.sandbox !== void 0) {
70335
+ const authoredSandbox = dropUserOnlySandboxKeys({
70336
+ sandbox: override.sandbox,
70337
+ global,
70338
+ relativeFilePath: relativeFilePathForLog,
70339
+ logger
70340
+ });
68664
70341
  const mergedSandbox = {
68665
70342
  ...asReasonixRecord(parsed.sandbox),
68666
- ...asReasonixRecord(override.sandbox)
70343
+ ...asReasonixRecord(authoredSandbox)
68667
70344
  };
68668
70345
  patch.sandbox = mergedSandbox;
68669
70346
  trustAffecting.push(...collectSandboxOverlayEntries({
68670
70347
  existing: parsed.sandbox,
68671
- authored: override.sandbox,
70348
+ authored: authoredSandbox,
68672
70349
  merged: mergedSandbox
68673
70350
  }));
68674
70351
  }
@@ -68721,7 +70398,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
68721
70398
  ask: askSplit.translated,
68722
70399
  deny: denySplit.translated
68723
70400
  });
68724
- const sandbox = asReasonixRecord(this.toml.sandbox);
70401
+ const sandbox = Object.fromEntries(Object.entries(asReasonixRecord(this.toml.sandbox)).filter(([key]) => this.global || !REASONIX_USER_ONLY_SANDBOX_KEYS.includes(key)));
68725
70402
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
68726
70403
  const reasonixOverride = {};
68727
70404
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
@@ -69027,7 +70704,7 @@ var RooPermissions = class extends ToolPermissions {
69027
70704
  };
69028
70705
  //#endregion
69029
70706
  //#region src/features/permissions/rovodev-permissions.ts
69030
- const CATCH_ALL_PATTERN$1 = "*";
70707
+ const CATCH_ALL_PATTERN$2 = "*";
69031
70708
  const CATEGORY_TO_TOOL_KEYS = {
69032
70709
  read: [
69033
70710
  "open_files",
@@ -69358,7 +71035,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
69358
71035
  logger
69359
71036
  });
69360
71037
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
69361
- if (category === CATCH_ALL_PATTERN$1) {
71038
+ if (category === CATCH_ALL_PATTERN$2) {
69362
71039
  const toolWideDefault = convertAllToolsRules({
69363
71040
  rules,
69364
71041
  logger
@@ -69377,7 +71054,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
69377
71054
  continue;
69378
71055
  }
69379
71056
  for (const [pattern, action] of Object.entries(rules)) {
69380
- if (pattern === CATCH_ALL_PATTERN$1) {
71057
+ if (pattern === CATCH_ALL_PATTERN$2) {
69381
71058
  toolPermissions.tools ??= {};
69382
71059
  for (const toolKey of toolKeys) toolPermissions.tools[toolKey] = strictestAction(toolPermissions.tools[toolKey], action);
69383
71060
  continue;
@@ -69400,8 +71077,8 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
69400
71077
  * resolution never grants more than the author asked for.
69401
71078
  */
69402
71079
  function warnOnEditWriteConflict({ config, logger }) {
69403
- const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
69404
- const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
71080
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$2];
71081
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$2];
69405
71082
  if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
69406
71083
  }
69407
71084
  /**
@@ -69414,7 +71091,7 @@ function warnOnEditWriteConflict({ config, logger }) {
69414
71091
  function convertAllToolsRules({ rules, logger }) {
69415
71092
  let toolWideDefault;
69416
71093
  for (const [pattern, action] of Object.entries(rules)) {
69417
- if (pattern === CATCH_ALL_PATTERN$1) {
71094
+ if (pattern === CATCH_ALL_PATTERN$2) {
69418
71095
  toolWideDefault = action;
69419
71096
  continue;
69420
71097
  }
@@ -69426,7 +71103,7 @@ function convertBashRules(rules) {
69426
71103
  const bash = {};
69427
71104
  const commands = [];
69428
71105
  for (const [pattern, action] of Object.entries(rules)) {
69429
- if (pattern === CATCH_ALL_PATTERN$1) {
71106
+ if (pattern === CATCH_ALL_PATTERN$2) {
69430
71107
  bash.default = action;
69431
71108
  continue;
69432
71109
  }
@@ -69443,26 +71120,26 @@ function convertBashRules(rules) {
69443
71120
  */
69444
71121
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
69445
71122
  const permission = {};
69446
- if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
71123
+ if (isPermissionAction$1(toolPermissions.default)) permission[CATCH_ALL_PATTERN$2] = { [CATCH_ALL_PATTERN$2]: toolPermissions.default };
69447
71124
  const bash = toolPermissions.bash;
69448
71125
  if (isRecord$1(bash)) {
69449
71126
  const bashRules = {};
69450
- if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
71127
+ if (isPermissionAction$1(bash.default)) bashRules[CATCH_ALL_PATTERN$2] = bash.default;
69451
71128
  if (Array.isArray(bash.commands)) {
69452
- for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
71129
+ for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction$1(entry.permission)) bashRules[entry.command] = entry.permission;
69453
71130
  }
69454
71131
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
69455
71132
  }
69456
71133
  const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
69457
- const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
71134
+ const implicitLevel = isPermissionAction$1(toolPermissions.default) ? toolPermissions.default : "ask";
69458
71135
  for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
69459
71136
  const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
69460
71137
  const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
69461
- return isPermissionAction(value) ? value : void 0;
71138
+ return isPermissionAction$1(value) ? value : void 0;
69462
71139
  });
69463
71140
  if (levels.every((level) => level === void 0)) continue;
69464
71141
  permission[category] ??= {};
69465
- permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
71142
+ permission[category][CATCH_ALL_PATTERN$2] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$2]);
69466
71143
  }
69467
71144
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
69468
71145
  permission.read ??= {};
@@ -69479,7 +71156,7 @@ function strictestAction(current, candidate) {
69479
71156
  if (current === void 0) return candidate;
69480
71157
  return ACTION_STRICTNESS.indexOf(current) <= ACTION_STRICTNESS.indexOf(candidate) ? current : candidate;
69481
71158
  }
69482
- function isPermissionAction(value) {
71159
+ function isPermissionAction$1(value) {
69483
71160
  return PermissionActionSchema.safeParse(value).success;
69484
71161
  }
69485
71162
  //#endregion
@@ -70186,7 +71863,7 @@ const TAKT_SECURITY_POLICIES = {
70186
71863
  const TAKT_SECURITY_POLICY_KEYS = Object.keys(TAKT_SECURITY_POLICIES);
70187
71864
  const TAKT_DEFAULT_PROVIDER = "claude-sdk";
70188
71865
  const TAKT_LEGACY_DEFAULT_PROVIDER = "claude";
70189
- const CATCH_ALL_PATTERN = "*";
71866
+ const CATCH_ALL_PATTERN$1 = "*";
70190
71867
  /**
70191
71868
  * Permissions adapter for Takt (`.takt/config.yaml`).
70192
71869
  *
@@ -70807,9 +72484,9 @@ function deriveTaktPermissionMode(config) {
70807
72484
  */
70808
72485
  function taktModeToRulesyncConfig(mode) {
70809
72486
  switch (mode) {
70810
- case "full": return { permission: { bash: { [CATCH_ALL_PATTERN]: "allow" } } };
70811
- case "edit": return { permission: { edit: { [CATCH_ALL_PATTERN]: "allow" } } };
70812
- default: return { permission: { bash: { [CATCH_ALL_PATTERN]: "deny" } } };
72487
+ case "full": return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "allow" } } };
72488
+ case "edit": return { permission: { edit: { [CATCH_ALL_PATTERN$1]: "allow" } } };
72489
+ default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
70813
72490
  }
70814
72491
  }
70815
72492
  /**
@@ -70842,6 +72519,409 @@ function pickSecurityPolicies(source, report) {
70842
72519
  return picked;
70843
72520
  }
70844
72521
  //#endregion
72522
+ //#region src/features/permissions/trae-permissions.ts
72523
+ const CUSTOM_PROFILES_KEY = "customProfiles";
72524
+ const DEFAULT_CUSTOM_PROFILE_KEY = "defaultCustomProfile";
72525
+ const APPROVAL_KEY = "approval";
72526
+ const COMMAND_RULES_KEY = "commandRules";
72527
+ const MCP_RULES_KEY = "mcpRules";
72528
+ const RESOURCE_AUTHORIZATION_KEY = "resourceAuthorization";
72529
+ const NETWORK_KEY = "network";
72530
+ const BASH_CATEGORY = "bash";
72531
+ const WEBFETCH_CATEGORY = "webfetch";
72532
+ const MCP_TOOL_PREFIX = "mcp__";
72533
+ const MCP_SEPARATOR = "__";
72534
+ const CATCH_ALL_PATTERN = "*";
72535
+ const REGEX_PREFIX = "r/";
72536
+ const REGEX_SUFFIX = "/";
72537
+ const ACTION_RANK = {
72538
+ allow: 0,
72539
+ ask: 1,
72540
+ deny: 2
72541
+ };
72542
+ const NETWORK_ACTIONS = ["allow", "deny"];
72543
+ function isPermissionAction(value) {
72544
+ return value === "allow" || value === "ask" || value === "deny";
72545
+ }
72546
+ function toStringList(value) {
72547
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
72548
+ }
72549
+ function escapeRegex(text) {
72550
+ return text.replace(/[\\^$.|?*+()[\]{}/]/g, "\\$&");
72551
+ }
72552
+ /** Build the anchored TRAE regex rule (`r/^...$/`) a canonical glob stands for. */
72553
+ function globToTraeRegex(pattern) {
72554
+ const body = pattern.split(CATCH_ALL_PATTERN).map(escapeRegex).join(".*");
72555
+ return `${REGEX_PREFIX}^${body}$${REGEX_SUFFIX}`;
72556
+ }
72557
+ /**
72558
+ * Translate a canonical `bash` glob into a TRAE `commandRules` key. TRAE
72559
+ * matches a key exactly (`git status`), as a prefix when it ends in `*`
72560
+ * (`git add *`), or as a regex when it is written `r/.../`. A glob whose only
72561
+ * `*` is the trailing one is already a TRAE prefix rule and a glob without
72562
+ * `*` an exact one; every other glob (a bare `*`, an inner `*`, or a literal
72563
+ * that would read as `r/.../`) becomes an anchored regex.
72564
+ */
72565
+ function toTraeCommandKey(pattern) {
72566
+ const starCount = pattern.split(CATCH_ALL_PATTERN).length - 1;
72567
+ const looksLikeRegex = pattern.startsWith(REGEX_PREFIX) && pattern.endsWith(REGEX_SUFFIX);
72568
+ if (!looksLikeRegex && starCount === 0) return pattern;
72569
+ if (!looksLikeRegex && starCount === 1 && pattern.length > 1 && pattern.endsWith("*")) return pattern;
72570
+ return globToTraeRegex(pattern);
72571
+ }
72572
+ /**
72573
+ * Read a `commandRules` key back into a canonical `bash` glob, or `null` for a
72574
+ * key with no glob spelling: a regex rulesync did not write, or an exact key
72575
+ * with an inner `*` (TRAE matches that `*` literally).
72576
+ */
72577
+ function fromTraeCommandKey(key) {
72578
+ if (!(key.startsWith(REGEX_PREFIX) && key.endsWith(REGEX_SUFFIX) && key.length > 2)) return toTraeCommandKey(key) === key ? key : null;
72579
+ const anchored = key.slice(2, -1).match(/^\^([\s\S]*)\$$/);
72580
+ if (!anchored) return null;
72581
+ const glob = (anchored[1] ?? "").split(".*").map((part) => part.replace(/\\(.)/g, "$1")).join(CATCH_ALL_PATTERN);
72582
+ return globToTraeRegex(glob) === key ? glob : null;
72583
+ }
72584
+ /**
72585
+ * Translate a canonical MCP category (`mcp__<server>__<tool>`,
72586
+ * `mcp__<server>__*` or `mcp__<server>`) into a TRAE `mcpRules` key
72587
+ * (`<server>__<tool>`, `<server>__*`, `<server>`). `null` when TRAE has no
72588
+ * spelling for it (`mcp__*`, which names no server).
72589
+ */
72590
+ function toTraeMcpKey(category) {
72591
+ const rest = category.slice(5);
72592
+ const server = rest.split(MCP_SEPARATOR)[0] ?? "";
72593
+ if (server === "" || server.includes(CATCH_ALL_PATTERN)) return null;
72594
+ return rest;
72595
+ }
72596
+ /** Read a TRAE `mcpRules` key back into its canonical MCP category. */
72597
+ function fromTraeMcpKey(key) {
72598
+ if (key === "" || key.startsWith(MCP_SEPARATOR)) return null;
72599
+ return key.includes(MCP_SEPARATOR) ? `${MCP_TOOL_PREFIX}${key}` : `${MCP_TOOL_PREFIX}${key}${MCP_SEPARATOR}${CATCH_ALL_PATTERN}`;
72600
+ }
72601
+ function readRecord(source, path) {
72602
+ let current = source;
72603
+ for (const key of path) {
72604
+ if (!isRecord$1(current)) return {};
72605
+ current = current[key];
72606
+ }
72607
+ return isRecord$1(current) ? current : {};
72608
+ }
72609
+ /**
72610
+ * Return a copy of `root` with `value` at `path` (or the leaf removed when
72611
+ * `value` is `undefined`), creating the parent objects only when setting.
72612
+ */
72613
+ function withPath(root, path, value) {
72614
+ const [head, ...rest] = path;
72615
+ if (head === void 0) return root;
72616
+ const next = { ...root };
72617
+ if (rest.length === 0) {
72618
+ if (value === void 0) delete next[head];
72619
+ else next[head] = value;
72620
+ return next;
72621
+ }
72622
+ const child = next[head];
72623
+ if (!isRecord$1(child)) {
72624
+ if (value === void 0) return next;
72625
+ next[head] = withPath({}, rest, value);
72626
+ return next;
72627
+ }
72628
+ next[head] = withPath(child, rest, value);
72629
+ return next;
72630
+ }
72631
+ const COMMAND_RULES_PATH = [
72632
+ DEFAULT_CUSTOM_PROFILE_KEY,
72633
+ APPROVAL_KEY,
72634
+ COMMAND_RULES_KEY
72635
+ ];
72636
+ const MCP_RULES_PATH = [
72637
+ DEFAULT_CUSTOM_PROFILE_KEY,
72638
+ APPROVAL_KEY,
72639
+ MCP_RULES_KEY
72640
+ ];
72641
+ function setRanked(ranked, key, action) {
72642
+ const existing = ranked.get(key);
72643
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(key, action);
72644
+ }
72645
+ /**
72646
+ * Rebuild `commandRules` from the canonical `bash` category. An existing
72647
+ * entry rulesync cannot read back (a hand-written regex) is kept, and the
72648
+ * `execEnv` of an entry this run rewrites is carried over.
72649
+ */
72650
+ function buildCommandRules({ bashRules, existing }) {
72651
+ const ranked = /* @__PURE__ */ new Map();
72652
+ for (const [pattern, action] of Object.entries(bashRules)) {
72653
+ if (isPrototypePollutionKey(pattern)) continue;
72654
+ setRanked(ranked, toTraeCommandKey(pattern), action);
72655
+ }
72656
+ const result = {};
72657
+ for (const [key, value] of Object.entries(existing)) if (!ranked.has(key) && fromTraeCommandKey(key) === null) result[key] = value;
72658
+ for (const [key, action] of ranked) {
72659
+ const previous = existing[key];
72660
+ const execEnv = isRecord$1(previous) ? previous.execEnv : void 0;
72661
+ result[key] = {
72662
+ approval: action,
72663
+ ...typeof execEnv === "string" && { execEnv }
72664
+ };
72665
+ }
72666
+ return result;
72667
+ }
72668
+ /**
72669
+ * Rebuild `mcpRules`: every MCP category of the canonical config is written,
72670
+ * and an existing entry whose category the config does not name is kept.
72671
+ */
72672
+ function buildMcpRules({ permission, existing, logger }) {
72673
+ const ranked = /* @__PURE__ */ new Map();
72674
+ const managedCategories = /* @__PURE__ */ new Set();
72675
+ for (const [category, rules] of Object.entries(permission)) {
72676
+ if (!category.startsWith(MCP_TOOL_PREFIX)) continue;
72677
+ const key = toTraeMcpKey(category);
72678
+ if (key === null) {
72679
+ logger?.warn(`TRAE permissions: skipping the '${category}' category — TRAE's mcpRules name an MCP server ('server', 'server__*' or 'server__tool').`);
72680
+ continue;
72681
+ }
72682
+ managedCategories.add(category);
72683
+ for (const [pattern, action] of Object.entries(rules)) {
72684
+ if (pattern !== CATCH_ALL_PATTERN) {
72685
+ logger?.warn(`TRAE permissions: skipping the '${category}' rule '${pattern}' — TRAE matches MCP tools by name only.`);
72686
+ continue;
72687
+ }
72688
+ setRanked(ranked, key, action);
72689
+ }
72690
+ }
72691
+ const result = {};
72692
+ for (const [key, value] of Object.entries(existing)) {
72693
+ const category = fromTraeMcpKey(key);
72694
+ if (!ranked.has(key) && (category === null || !managedCategories.has(category))) result[key] = value;
72695
+ }
72696
+ for (const [key, action] of ranked) result[key] = { approval: action };
72697
+ return result;
72698
+ }
72699
+ /** Split the canonical `webfetch` rules into TRAE's network allow/deny lists. */
72700
+ function buildNetworkLists({ webfetchRules, logger }) {
72701
+ const ranked = /* @__PURE__ */ new Map();
72702
+ for (const [pattern, action] of Object.entries(webfetchRules)) {
72703
+ if (isPrototypePollutionKey(pattern)) continue;
72704
+ if (pattern === CATCH_ALL_PATTERN) {
72705
+ logger?.warn(`TRAE permissions: skipping the 'webfetch' rule '*' — TRAE sets the default network policy in the custom permission mode ('customProfiles.defaultCustomProfile.network.default'), not in resourceAuthorization.`);
72706
+ continue;
72707
+ }
72708
+ if (action === "ask") {
72709
+ logger?.warn(`TRAE permissions: skipping the 'webfetch' ask rule '${pattern}' — resourceAuthorization.network has only allow and deny lists.`);
72710
+ continue;
72711
+ }
72712
+ setRanked(ranked, pattern, action);
72713
+ }
72714
+ const lists = {
72715
+ allow: [],
72716
+ deny: []
72717
+ };
72718
+ for (const [pattern, action] of ranked) if (action === "allow" || action === "deny") lists[action].push(pattern);
72719
+ return lists;
72720
+ }
72721
+ function warnUnmappedCategories({ permission, logger }) {
72722
+ for (const category of Object.keys(permission)) if (category !== BASH_CATEGORY && category !== WEBFETCH_CATEGORY && !category.startsWith(MCP_TOOL_PREFIX)) logger?.warn(`TRAE permissions: skipping the '${category}' category — TRAE's global permission file has no rule for it (only 'bash', 'webfetch' and 'mcp__*' are mapped).`);
72723
+ }
72724
+ /** A rebuilt map, or `undefined` (remove the key) when it ended up empty. */
72725
+ function nonEmpty(value) {
72726
+ return Object.keys(value).length > 0 ? value : void 0;
72727
+ }
72728
+ /**
72729
+ * The new `customProfiles` value: the existing one with `commandRules` and
72730
+ * `mcpRules` rebuilt for the categories the config names. The existing value
72731
+ * is returned untouched when the config names none of them.
72732
+ */
72733
+ function buildCustomProfiles({ existing, permission, logger }) {
72734
+ const bashRules = permission[BASH_CATEGORY];
72735
+ const managesMcp = Object.keys(permission).some((category) => category.startsWith(MCP_TOOL_PREFIX));
72736
+ if (bashRules === void 0 && !managesMcp) return existing;
72737
+ let result = isRecord$1(existing) ? existing : {};
72738
+ if (bashRules !== void 0) {
72739
+ const commandRules = buildCommandRules({
72740
+ bashRules,
72741
+ existing: readRecord(result, COMMAND_RULES_PATH)
72742
+ });
72743
+ result = withPath(result, COMMAND_RULES_PATH, nonEmpty(commandRules));
72744
+ }
72745
+ if (managesMcp) {
72746
+ const mcpRules = buildMcpRules({
72747
+ permission,
72748
+ existing: readRecord(result, MCP_RULES_PATH),
72749
+ logger
72750
+ });
72751
+ result = withPath(result, MCP_RULES_PATH, nonEmpty(mcpRules));
72752
+ }
72753
+ return existing === void 0 ? nonEmpty(result) : result;
72754
+ }
72755
+ /**
72756
+ * The new `resourceAuthorization` value: the existing one with
72757
+ * `network.allow` / `deny` rebuilt from the `webfetch` category, or the
72758
+ * existing value untouched when the config has no `webfetch` category.
72759
+ */
72760
+ function buildResourceAuthorization({ existing, permission, logger }) {
72761
+ const webfetchRules = permission[WEBFETCH_CATEGORY];
72762
+ if (webfetchRules === void 0) return existing;
72763
+ const lists = buildNetworkLists({
72764
+ webfetchRules,
72765
+ logger
72766
+ });
72767
+ let result = isRecord$1(existing) ? existing : {};
72768
+ for (const action of NETWORK_ACTIONS) result = withPath(result, [NETWORK_KEY, action], nonEmpty(lists[action]));
72769
+ return existing === void 0 ? nonEmpty(result) : result;
72770
+ }
72771
+ /**
72772
+ * TRAE permissions (global scope only).
72773
+ *
72774
+ * TRAE keeps its global permission policy and its custom permission mode in
72775
+ * one user file, `~/.trae/permission/global.json`. Rulesync writes three parts
72776
+ * of it and leaves the rest (`shellSandbox`, `approval.reviewer`,
72777
+ * `sceneRules`, the default `filesystem` / `network` policy,
72778
+ * `resourceAuthorization.filesystem`) as it is:
72779
+ *
72780
+ * - `bash` → `customProfiles.defaultCustomProfile.approval.commandRules`,
72781
+ * `{ "<pattern>": { "approval": "allow" | "ask" | "deny" } }`. A pattern is
72782
+ * matched exactly, as a prefix when it ends in `*`, or as a regex written
72783
+ * `r/.../`; other globs become anchored regexes.
72784
+ * - `mcp__<server>__<tool>` / `mcp__<server>__*` →
72785
+ * `customProfiles.defaultCustomProfile.approval.mcpRules`
72786
+ * (`<server>__<tool>` / `<server>__*`).
72787
+ * - `webfetch` domains → `resourceAuthorization.network.allow` / `deny`.
72788
+ *
72789
+ * The file is never deleted, and an unparseable file is an error rather than
72790
+ * being replaced.
72791
+ *
72792
+ * @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
72793
+ * @see https://docs.trae.ai/ide/custom-global-permission-configuration-reference?_lang=en
72794
+ * @see https://docs.trae.ai/ide/custom-permission-mode-configuration-reference?_lang=en
72795
+ */
72796
+ var TraePermissions = class TraePermissions extends ToolPermissions {
72797
+ constructor(params) {
72798
+ super({
72799
+ ...params,
72800
+ fileContent: params.fileContent ?? "{}"
72801
+ });
72802
+ }
72803
+ isDeletable() {
72804
+ return false;
72805
+ }
72806
+ static getSettablePaths(_options = {}) {
72807
+ return {
72808
+ relativeDirPath: TRAE_PERMISSION_DIR_PATH,
72809
+ relativeFilePath: TRAE_PERMISSION_FILE_NAME
72810
+ };
72811
+ }
72812
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = true }) {
72813
+ const paths = TraePermissions.getSettablePaths({ global });
72814
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
72815
+ return new TraePermissions({
72816
+ outputRoot,
72817
+ relativeDirPath: paths.relativeDirPath,
72818
+ relativeFilePath: paths.relativeFilePath,
72819
+ fileContent,
72820
+ validate,
72821
+ global
72822
+ });
72823
+ }
72824
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = true, logger }) {
72825
+ const paths = TraePermissions.getSettablePaths({ global });
72826
+ const relativePath = join(paths.relativeDirPath, paths.relativeFilePath);
72827
+ const filePath = join(outputRoot, relativePath);
72828
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
72829
+ const existing = parseTraePermissionConfig({
72830
+ fileContent: existingContent,
72831
+ relativePath
72832
+ });
72833
+ const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
72834
+ warnUnmappedCategories({
72835
+ permission,
72836
+ logger
72837
+ });
72838
+ return new TraePermissions({
72839
+ outputRoot,
72840
+ relativeDirPath: paths.relativeDirPath,
72841
+ relativeFilePath: paths.relativeFilePath,
72842
+ fileContent: applySharedConfigPatch({
72843
+ fileKey: sharedConfigFileKey(paths),
72844
+ feature: "permissions",
72845
+ existingContent,
72846
+ patch: {
72847
+ [CUSTOM_PROFILES_KEY]: buildCustomProfiles({
72848
+ existing: existing[CUSTOM_PROFILES_KEY],
72849
+ permission,
72850
+ logger
72851
+ }),
72852
+ [RESOURCE_AUTHORIZATION_KEY]: buildResourceAuthorization({
72853
+ existing: existing[RESOURCE_AUTHORIZATION_KEY],
72854
+ permission,
72855
+ logger
72856
+ })
72857
+ },
72858
+ filePath
72859
+ }),
72860
+ validate: true,
72861
+ global
72862
+ });
72863
+ }
72864
+ toRulesyncPermissions() {
72865
+ const config = parseTraePermissionConfig({
72866
+ fileContent: this.getFileContent() || "{}",
72867
+ relativePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
72868
+ });
72869
+ const customProfiles = readRecord(config, [CUSTOM_PROFILES_KEY]);
72870
+ const permission = {};
72871
+ const put = (category, pattern, action) => {
72872
+ if (isPrototypePollutionKey(category) || isPrototypePollutionKey(pattern)) return;
72873
+ const rules = permission[category] ??= {};
72874
+ const existing = rules[pattern];
72875
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[pattern] = action;
72876
+ };
72877
+ for (const [key, value] of Object.entries(readRecord(customProfiles, COMMAND_RULES_PATH))) {
72878
+ const pattern = fromTraeCommandKey(key);
72879
+ const approval = isRecord$1(value) ? value.approval : void 0;
72880
+ if (pattern !== null && pattern.trim() !== "" && isPermissionAction(approval)) put(BASH_CATEGORY, pattern, approval);
72881
+ }
72882
+ for (const [key, value] of Object.entries(readRecord(customProfiles, MCP_RULES_PATH))) {
72883
+ const category = fromTraeMcpKey(key);
72884
+ const approval = isRecord$1(value) ? value.approval : void 0;
72885
+ if (category !== null && isPermissionAction(approval)) put(category, CATCH_ALL_PATTERN, approval);
72886
+ }
72887
+ const network = readRecord(config, [RESOURCE_AUTHORIZATION_KEY, NETWORK_KEY]);
72888
+ for (const action of NETWORK_ACTIONS) for (const pattern of toStringList(network[action])) if (pattern.trim() !== "") put(WEBFETCH_CATEGORY, pattern, action);
72889
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
72890
+ }
72891
+ validate() {
72892
+ return {
72893
+ success: true,
72894
+ error: null
72895
+ };
72896
+ }
72897
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = true }) {
72898
+ return new TraePermissions({
72899
+ outputRoot,
72900
+ relativeDirPath,
72901
+ relativeFilePath,
72902
+ fileContent: "{}",
72903
+ validate: false,
72904
+ global
72905
+ });
72906
+ }
72907
+ };
72908
+ /**
72909
+ * Fail closed on a syntax error or non-object root, so a broken file is
72910
+ * surfaced rather than partially imported or overwritten.
72911
+ */
72912
+ function parseTraePermissionConfig({ fileContent, relativePath }) {
72913
+ try {
72914
+ return parseSharedConfig({
72915
+ format: "json",
72916
+ fileContent,
72917
+ filePath: relativePath,
72918
+ invalidRootPolicy: "error"
72919
+ });
72920
+ } catch (error) {
72921
+ throw new Error(`Failed to parse TRAE permission config in ${relativePath}: ${formatError(error)}`, { cause: error });
72922
+ }
72923
+ }
72924
+ //#endregion
70845
72925
  //#region src/utils/display-width.ts
70846
72926
  /**
70847
72927
  * Characters a terminal draws two columns wide: the East Asian Wide and
@@ -74127,6 +76207,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
74127
76207
  supportsImport: true
74128
76208
  }
74129
76209
  }],
76210
+ ["trae", {
76211
+ class: TraePermissions,
76212
+ meta: {
76213
+ supportsProject: false,
76214
+ supportsGlobal: true,
76215
+ supportsImport: true
76216
+ }
76217
+ }],
74130
76218
  ["zoocode", {
74131
76219
  class: ZoocodePermissions,
74132
76220
  meta: {
@@ -74231,7 +76319,7 @@ var PermissionsProcessor = class extends FeatureProcessor {
74231
76319
  })];
74232
76320
  }
74233
76321
  async convertToolFilesToRulesyncFiles(toolFiles) {
74234
- return toolFiles.filter((f) => f instanceof ToolPermissions).map((p) => p.toRulesyncPermissions());
76322
+ return toolFiles.filter((f) => f instanceof ToolPermissions).map((p) => p.toRulesyncPermissions({ logger: this.logger }));
74235
76323
  }
74236
76324
  static getToolTargets({ global = false, importOnly = false } = {}) {
74237
76325
  return [...toolPermissionsFactories.entries()].filter(([, f]) => global ? f.meta.supportsGlobal : f.meta.supportsProject).filter(([, f]) => importOnly ? f.meta.supportsImport : true).map(([target]) => target);
@@ -79144,8 +81232,13 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
79144
81232
  * slash command), so this target reuses {@link AgentsSkillsSkill} with GitLab's
79145
81233
  * locations:
79146
81234
  *
79147
- * - Project skills live in a non-hidden `skills/<name>/SKILL.md` at the project
79148
- * root, which both the GitLab UI flows and the GitLab Duo CLI read.
81235
+ * - Project skills are written to the cross-tool `.agents/skills/<name>/SKILL.md`
81236
+ * root, which both the GitLab UI flows and the GitLab Duo CLI read. GitLab
81237
+ * Duo also reads a non-hidden `skills/<name>/SKILL.md` at the project root
81238
+ * (`.agents/skills/` wins on a name clash); rulesync imports from it but
81239
+ * never writes or sweeps it, because that directory routinely holds
81240
+ * hand-authored skills. Earlier rulesync versions wrote there, so a skill
81241
+ * generated by one of them has to be removed from `skills/` by hand.
79149
81242
  * - User skills live in `~/.gitlab/duo/skills/` and are loaded by the CLI only
79150
81243
  * when started with `--enable-global-skills true` (or
79151
81244
  * `GITLAB_ENABLE_GLOBAL_SKILLS=true`).
@@ -79154,18 +81247,20 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
79154
81247
  */
79155
81248
  var GitlabduoSkill = class extends AgentsSkillsSkill {
79156
81249
  static getSettablePaths({ global = false } = {}) {
79157
- return { relativeDirPath: global ? GITLABDUO_GLOBAL_SKILLS_DIR : GITLABDUO_SKILLS_DIR };
81250
+ if (global) return { relativeDirPath: GITLABDUO_GLOBAL_SKILLS_DIR };
81251
+ return {
81252
+ relativeDirPath: GITLABDUO_SKILLS_DIR,
81253
+ importOnlySkillRoots: [GITLABDUO_LEGACY_SKILLS_DIR]
81254
+ };
79158
81255
  }
79159
81256
  static isTargetedByRulesyncSkill(rulesyncSkill) {
79160
81257
  const targets = rulesyncSkill.getFrontmatter().targets;
79161
81258
  return targets.includes("*") || targets.includes("gitlabduo");
79162
81259
  }
79163
81260
  /**
79164
- * The project skills root is a plain `skills/` directory that many
79165
- * repositories already use for unrelated content, so only a subdirectory
79166
- * holding a `SKILL.md` is treated as a skill. (At project scope the
79167
- * `--delete` orphan sweep skips that root entirely through the factory's
79168
- * `committedOutput` meta; this filter still applies to the global sweep.)
81261
+ * The import-only project-root `skills/` directory is one many repositories
81262
+ * already use for unrelated content, so only a subdirectory holding a
81263
+ * `SKILL.md` is treated as a skill, in every root this target reads.
79169
81264
  */
79170
81265
  static async isDirOwned({ outputRoot, relativeDirPath, dirName }) {
79171
81266
  return await fileExists(join(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME));
@@ -80718,9 +82813,44 @@ var MimocodeSkill = class extends OpenCodeSkill {
80718
82813
  //#region src/features/skills/musecode-skill.ts
80719
82814
  const MusecodeSkillFrontmatterSchema = z.looseObject({
80720
82815
  name: z.string(),
80721
- description: z.string()
82816
+ description: z.string(),
82817
+ license: z.optional(z.string()),
82818
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
82819
+ metadata: z.optional(z.looseObject({}))
80722
82820
  });
80723
82821
  /**
82822
+ * Build the Muse Code frontmatter. `license`, `compatibility` and `metadata`
82823
+ * come from the `musecode` section with the root-level values as fallback, and
82824
+ * are normalized exactly as the `agentsskills` writer normalizes them: at
82825
+ * project scope both targets write the same `.agents/skills/<name>/SKILL.md`,
82826
+ * so for a skill without per-target overrides the file no longer depends on
82827
+ * which of the two runs last.
82828
+ */
82829
+ function buildMusecodeFrontmatter(rulesyncFrontmatter) {
82830
+ const section = rulesyncFrontmatter.musecode;
82831
+ const license = resolveLicense({
82832
+ rootFrontmatter: rulesyncFrontmatter,
82833
+ section
82834
+ });
82835
+ const compatibility = resolveCompatibility({
82836
+ rootFrontmatter: rulesyncFrontmatter,
82837
+ section
82838
+ });
82839
+ const metadata = resolveMetadata({
82840
+ rootFrontmatter: rulesyncFrontmatter,
82841
+ section
82842
+ });
82843
+ return {
82844
+ name: rulesyncFrontmatter.name,
82845
+ description: rulesyncFrontmatter.description,
82846
+ ...toSpecConformantAgentSkillFields({
82847
+ ...license !== void 0 && { license },
82848
+ ...compatibility !== void 0 && { compatibility },
82849
+ ...metadata !== void 0 && { metadata }
82850
+ })
82851
+ };
82852
+ }
82853
+ /**
80724
82854
  * Represents a Meta Muse Code skill directory.
80725
82855
  *
80726
82856
  * Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
@@ -80777,10 +82907,16 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
80777
82907
  }
80778
82908
  toRulesyncSkill() {
80779
82909
  const frontmatter = this.getFrontmatter();
82910
+ const musecodeSection = {
82911
+ ...frontmatter.license !== void 0 && { license: frontmatter.license },
82912
+ ...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
82913
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
82914
+ };
80780
82915
  const rulesyncFrontmatter = {
80781
82916
  name: frontmatter.name,
80782
82917
  description: frontmatter.description,
80783
- targets: ["*"]
82918
+ targets: ["*"],
82919
+ ...Object.keys(musecodeSection).length > 0 && { musecode: musecodeSection }
80784
82920
  };
80785
82921
  return new RulesyncSkill({
80786
82922
  outputRoot: this.outputRoot,
@@ -80795,11 +82931,7 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
80795
82931
  }
80796
82932
  static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
80797
82933
  const settablePaths = MusecodeSkill.getSettablePaths({ global });
80798
- const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
80799
- const musecodeFrontmatter = {
80800
- name: rulesyncFrontmatter.name,
80801
- description: rulesyncFrontmatter.description
80802
- };
82934
+ const musecodeFrontmatter = buildMusecodeFrontmatter(rulesyncSkill.getFrontmatter());
80803
82935
  return new MusecodeSkill({
80804
82936
  outputRoot,
80805
82937
  relativeDirPath: settablePaths.relativeDirPath,
@@ -83250,8 +85382,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
83250
85382
  supportsProject: true,
83251
85383
  supportsSimulated: false,
83252
85384
  supportsGlobal: true,
83253
- lenientImport: true,
83254
- committedOutput: true
85385
+ lenientImport: true
83255
85386
  }
83256
85387
  }],
83257
85388
  ["grokcli", {
@@ -83864,10 +85995,6 @@ var SkillsProcessor = class extends DirFeatureProcessor {
83864
85995
  }
83865
85996
  async loadToolDirsToDelete() {
83866
85997
  const factory = this.getFactory(this.toolTarget);
83867
- if (factory.meta.committedOutput === true && !this.global) {
83868
- this.logger.debug(`Skipping the orphan sweep for ${this.toolTarget} skills: the project root is committed and user-owned`);
83869
- return [];
83870
- }
83871
85998
  const paths = factory.class.getSettablePaths({ global: this.global });
83872
85999
  const roots = toolSkillSearchRoots(paths);
83873
86000
  const toolSkills = [];
@@ -85412,6 +87539,14 @@ const PLUGIN_FORBIDDEN_FIELDS = [
85412
87539
  "mcpServers",
85413
87540
  "permissionMode"
85414
87541
  ];
87542
+ /**
87543
+ * Claude Code ignores these for plugin-shipped agents without any security
87544
+ * rationale. They are dropped with a separate warning so the output does not
87545
+ * suggest a behavior the plugin agent never gets.
87546
+ *
87547
+ * @see https://code.claude.com/docs/en/sub-agents#supported-frontmatter-fields
87548
+ */
87549
+ const PLUGIN_IGNORED_FIELDS = ["initialPrompt"];
85415
87550
  /** The only `isolation` value plugin agents accept. */
85416
87551
  const PLUGIN_ISOLATION_VALUE = "worktree";
85417
87552
  var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
@@ -85431,6 +87566,9 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
85431
87566
  const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
85432
87567
  for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
85433
87568
  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.`);
87569
+ const ignored = PLUGIN_IGNORED_FIELDS.filter((field) => sanitized[field] !== void 0);
87570
+ for (const field of PLUGIN_IGNORED_FIELDS) delete sanitized[field];
87571
+ if (ignored.length > 0) logger?.warn(`Dropping ${ignored.join(", ")} from claudecode-plugin subagent ${relativeFilePath}: Claude Code ignores these fields for plugin-shipped agents.`);
85434
87572
  if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
85435
87573
  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.`);
85436
87574
  delete sanitized.isolation;
@@ -88884,6 +91022,32 @@ async function resolveOpenCodeAgentPrompt({ prompt, configDir }) {
88884
91022
  configDir
88885
91023
  });
88886
91024
  }
91025
+ /**
91026
+ * Resolves the `prompt` of each inline agent entry of one config file against
91027
+ * that file's directory, so entries from several files can be merged without
91028
+ * losing where a `"{file:./path}"` reference points.
91029
+ */
91030
+ async function resolveOpenCodeAgentPrompts({ entries, configDir }) {
91031
+ if (entries === null) return null;
91032
+ const resolved = {};
91033
+ for (const [name, rawEntry] of Object.entries(entries)) {
91034
+ const entry = asOpencodeEntries(rawEntry);
91035
+ const value = entry === null || !Object.hasOwn(entry, "prompt") ? rawEntry : {
91036
+ ...entry,
91037
+ prompt: await resolveOpenCodeAgentPrompt({
91038
+ prompt: entry.prompt,
91039
+ configDir
91040
+ })
91041
+ };
91042
+ Object.defineProperty(resolved, name, {
91043
+ value,
91044
+ enumerable: true,
91045
+ configurable: true,
91046
+ writable: true
91047
+ });
91048
+ }
91049
+ return resolved;
91050
+ }
88887
91051
  var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
88888
91052
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
88889
91053
  static layout = OPENCODE_LAYOUT;
@@ -88955,8 +91119,9 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
88955
91119
  });
88956
91120
  }
88957
91121
  /**
88958
- * Imports agents defined inline in `opencode.json` / `opencode.jsonc` under
88959
- * the top-level `agent` key — or its V2 spelling `agents`, see
91122
+ * Imports agents defined inline in `opencode.json` / `opencode.jsonc` (and,
91123
+ * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
91124
+ * them) under the top-level `agent` key — or its V2 spelling `agents`, see
88960
91125
  * `getOpencodeAgentEntries` — (in addition to the Markdown files under
88961
91126
  * `.opencode/agents/`). Each entry's `prompt` becomes the subagent body
88962
91127
  * (a `"{file:./path}"` reference is resolved relative to the config file's
@@ -88969,30 +91134,26 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
88969
91134
  * @see https://opencode.ai/docs/agents/#json
88970
91135
  */
88971
91136
  static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false } = {}) {
88972
- const agentEntries = getOpencodeAgentEntries({
88973
- config: await readOpencodeConfig({
88974
- outputRoot,
88975
- global,
88976
- layout: this.layout
88977
- }),
88978
- layout: this.layout
88979
- });
88980
- if (!agentEntries) return [];
88981
- const paths = this.getSettablePaths({ global });
88982
- const configDir = getOpencodeConfigDir({
91137
+ const layers = await readOpencodeConfigLayers({
88983
91138
  outputRoot,
88984
91139
  global,
88985
91140
  layout: this.layout
88986
91141
  });
91142
+ const agentEntries = mergeOpencodeEntryLayers(await Promise.all(layers.map(({ config, configDir }) => resolveOpenCodeAgentPrompts({
91143
+ entries: getOpencodeAgentEntries({
91144
+ config,
91145
+ layout: this.layout
91146
+ }),
91147
+ configDir
91148
+ }))));
91149
+ if (!agentEntries) return [];
91150
+ const paths = this.getSettablePaths({ global });
88987
91151
  const subagents = [];
88988
91152
  for (const [name, rawEntry] of Object.entries(agentEntries)) {
88989
91153
  const entry = asOpencodeEntries(rawEntry);
88990
91154
  if (!entry) continue;
88991
91155
  const { prompt, ...frontmatterFields } = entry;
88992
- const body = await resolveOpenCodeAgentPrompt({
88993
- prompt,
88994
- configDir
88995
- });
91156
+ const body = typeof prompt === "string" ? prompt : "";
88996
91157
  const parseResult = OpenCodeSubagentFrontmatterSchema.safeParse(frontmatterFields);
88997
91158
  if (!parseResult.success) continue;
88998
91159
  const frontmatter = parseResult.data;
@@ -91752,7 +93913,7 @@ var ToolRule = class extends ToolFile {
91752
93913
  body: this.getFileContent()
91753
93914
  });
91754
93915
  }
91755
- toRulesyncRuleDefault() {
93916
+ toRulesyncRuleDefault({ body } = {}) {
91756
93917
  return new RulesyncRule({
91757
93918
  outputRoot: process.cwd(),
91758
93919
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -91763,7 +93924,7 @@ var ToolRule = class extends ToolFile {
91763
93924
  description: this.description,
91764
93925
  globs: this.globs ?? (this.isRoot() ? ["**/*"] : [])
91765
93926
  },
91766
- body: this.getFileContent()
93927
+ body: body ?? this.getFileContent()
91767
93928
  });
91768
93929
  }
91769
93930
  isRoot() {
@@ -92869,11 +95030,19 @@ var AntigravityIdeRule = class extends ToolRule {
92869
95030
  }
92870
95031
  super({
92871
95032
  ...rest,
92872
- fileContent: rest.root ? body : stringifyFrontmatter(body, frontmatter)
95033
+ fileContent: rest.root && Object.keys(frontmatter).length === 0 ? body : stringifyFrontmatter(body, frontmatter)
92873
95034
  });
92874
95035
  this.frontmatter = frontmatter;
92875
95036
  this.body = body;
92876
95037
  }
95038
+ /**
95039
+ * Frontmatter written on the root rule. The project `AGENTS.md` and the
95040
+ * global `GEMINI.md` take none; a subclass whose root rule sits inside a
95041
+ * `rules/` directory, where every `.md` needs a `trigger`, overrides it.
95042
+ */
95043
+ static getRootRuleFrontmatter() {
95044
+ return {};
95045
+ }
92877
95046
  static getGlobalRootPath(excludeToolDir) {
92878
95047
  return {
92879
95048
  relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_GEMINI_DIR, ".", excludeToolDir),
@@ -92900,13 +95069,16 @@ var AntigravityIdeRule = class extends ToolRule {
92900
95069
  const paths = this.getSettablePaths({ global });
92901
95070
  if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
92902
95071
  const rootPath = paths.root;
92903
- const rootContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
95072
+ const rootFilePath = join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath);
95073
+ const rootContent = await readFileContent(rootFilePath);
95074
+ const rootFrontmatter = this.getRootRuleFrontmatter();
95075
+ const rootBody = Object.keys(rootFrontmatter).length === 0 ? rootContent : parseFrontmatter(rootContent, rootFilePath).body;
92904
95076
  return new this({
92905
95077
  outputRoot,
92906
95078
  relativeDirPath: rootPath.relativeDirPath,
92907
95079
  relativeFilePath: rootPath.relativeFilePath,
92908
- frontmatter: {},
92909
- body: rootContent,
95080
+ frontmatter: rootFrontmatter,
95081
+ body: rootBody,
92910
95082
  validate,
92911
95083
  root: true,
92912
95084
  global
@@ -92966,7 +95138,7 @@ var AntigravityIdeRule = class extends ToolRule {
92966
95138
  outputRoot,
92967
95139
  relativeDirPath: rootPath.relativeDirPath,
92968
95140
  relativeFilePath: rootPath.relativeFilePath,
92969
- frontmatter: {},
95141
+ frontmatter: this.getRootRuleFrontmatter(),
92970
95142
  body: rulesyncRule.getBody(),
92971
95143
  validate,
92972
95144
  root: true,
@@ -92997,7 +95169,7 @@ var AntigravityIdeRule = class extends ToolRule {
92997
95169
  });
92998
95170
  }
92999
95171
  toRulesyncRule() {
93000
- if (this.root) return this.toRulesyncRuleDefault();
95172
+ if (this.root) return this.toRulesyncRuleDefault({ body: this.body });
93001
95173
  const strategy = STRATEGIES$1.find((s) => s.canHandle(this.frontmatter.trigger));
93002
95174
  let rulesyncData = {
93003
95175
  globs: [],
@@ -93075,6 +95247,9 @@ var AntigravityPluginRule = class extends AntigravityIdeRule {
93075
95247
  static supportsSubprojectRules() {
93076
95248
  return false;
93077
95249
  }
95250
+ static getRootRuleFrontmatter() {
95251
+ return { trigger: "always_on" };
95252
+ }
93078
95253
  static isTargetedByRulesyncRule(rulesyncRule) {
93079
95254
  const targets = rulesyncRule.getFrontmatter().targets;
93080
95255
  return targets.includes("*") || targets.includes("antigravity-plugin");
@@ -94664,8 +96839,11 @@ var CodexcliRule = class extends NestedAgentsmdRule {
94664
96839
  * `.codex/config.toml`, which Codex loads for trusted projects and which
94665
96840
  * overrides the 32 KiB default budget for project `AGENTS.md` files.
94666
96841
  *
94667
- * Answers `undefined` when the file is missing, is not valid TOML, or does not
94668
- * set the key to a positive integer, so the caller falls back to the default.
96842
+ * Answers `0` when the key is set to `0`, which Codex treats as "project docs
96843
+ * disabled" (`codex-rs/core/src/agents_md.rs` loads nothing for a zero
96844
+ * budget). Answers `undefined` when the file is missing, is not valid TOML, or
96845
+ * does not set the key to a non-negative integer, so the caller falls back to
96846
+ * the default.
94669
96847
  *
94670
96848
  * @see https://learn.chatgpt.com/docs/config-file/config-reference
94671
96849
  */
@@ -94680,7 +96858,7 @@ async function readCodexcliProjectDocMaxBytes(outputRoot) {
94680
96858
  }
94681
96859
  if (!isPlainObject$1(parsed)) return;
94682
96860
  const value = parsed.project_doc_max_bytes;
94683
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
96861
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
94684
96862
  }
94685
96863
  //#endregion
94686
96864
  //#region src/features/rules/commandcode-rule.ts
@@ -96042,10 +98220,13 @@ var DevinPluginRule = class extends DevinRule {
96042
98220
  * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
96043
98221
  * files — the project chain `dsh-agent-instructions` loads from the `.git`
96044
98222
  * root down to the session cwd, broad to specific, so a per-directory file
96045
- * applies only while working under that directory. Global scope writes the
96046
- * user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
96047
- * reads as a single root file with no `.local.md` overlay, so non-root rules
96048
- * fold into it.
98223
+ * applies only while working under that directory. A `localRoot` rule goes to
98224
+ * the project-root `AGENTS.local.md`, the additive local overlay the project
98225
+ * chain loads beside each `AGENTS.md`. Global scope writes the user-global
98226
+ * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a single
98227
+ * root file with no `.local.md` overlay, so non-root rules fold into it. The
98228
+ * harness also reads a shared `~/.agents/AGENTS.md` (`$DSH_AGENTS_HOME`) after
98229
+ * it, which rulesync never writes.
96049
98230
  *
96050
98231
  * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
96051
98232
  */
@@ -97716,10 +99897,6 @@ var OmpRule = class OmpRule extends ToolRule {
97716
99897
  }
97717
99898
  };
97718
99899
  //#endregion
97719
- //#region src/constants/openclaw-paths.ts
97720
- const OPENCLAW_WORKSPACE_DIR_PATH = join(".openclaw", "workspace");
97721
- const OPENCLAW_RULE_FILE_NAME = "AGENTS.md";
97722
- //#endregion
97723
99900
  //#region src/features/rules/openclaw-rule.ts
97724
99901
  var OpenclawRule = class OpenclawRule extends ToolRule {
97725
99902
  constructor({ fileContent, root, ...rest }) {
@@ -99857,6 +102034,39 @@ const normalizeRelativeDir = (dir) => posix.normalize(toPosixPath(dir)).replace(
99857
102034
  /** A tool rule's output path relative to its output root, for comparison. */
99858
102035
  const toolOutputPath = (toolRule) => posix.join(normalizeRelativeDir(toolRule.getRelativeDirPath()), toolRule.getRelativeFilePath());
99859
102036
  const formatRulePaths = (rules) => rules.map((r) => join(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
102037
+ /**
102038
+ * The leading `---` frontmatter block (with its closing delimiter line) that
102039
+ * every one of `contents` starts with, or `undefined` when one lacks it or the
102040
+ * blocks differ.
102041
+ */
102042
+ const findSharedLeadingFrontmatter = (contents) => {
102043
+ const blocks = contents.map((content) => /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.exec(content)?.[0]);
102044
+ const first = blocks[0];
102045
+ return first !== void 0 && blocks.every((block) => block === first) ? first : void 0;
102046
+ };
102047
+ /**
102048
+ * Whether a group of rules colliding on one output path is composed into one
102049
+ * file, and the frontmatter block the composed file keeps, if any.
102050
+ */
102051
+ const decideRuleComposition = ({ group, collisionPolicy }) => {
102052
+ const contents = group.map(({ toolRule }) => toolRule.getFileContent());
102053
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
102054
+ const allFragmentsArePlain = contents.every((content) => !/^---\r?\n/.test(content));
102055
+ const sharedRootFrontmatter = allGeneratedRulesAreRoots ? findSharedLeadingFrontmatter(contents) : void 0;
102056
+ return {
102057
+ shouldCompose: (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain || sharedRootFrontmatter !== void 0,
102058
+ sharedRootFrontmatter
102059
+ };
102060
+ };
102061
+ /**
102062
+ * Concatenate colliding rule fragments. With `sharedFrontmatter` (the block
102063
+ * every fragment starts with), the block is stripped from each fragment and
102064
+ * written once at the top.
102065
+ */
102066
+ const composeRuleFragments = ({ contents, sharedFrontmatter }) => {
102067
+ const merged = contents.map((content) => content.slice(sharedFrontmatter?.length ?? 0).trim()).filter((content) => content.length > 0).join("\n\n");
102068
+ return sharedFrontmatter === void 0 ? merged : `${sharedFrontmatter}${merged}\n`;
102069
+ };
99860
102070
  const RulesFeatureOptionsSchema = z.looseObject({
99861
102071
  ruleDiscoveryMode: z.optional(z.enum(["none", "explicit"])),
99862
102072
  includeLocalRoot: z.optional(z.boolean()),
@@ -100056,7 +102266,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
100056
102266
  collisionPolicy: "fold",
100057
102267
  projectInstructionBudget: {
100058
102268
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
100059
- remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
102269
+ remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default): it silently truncates the file that crosses the limit and skips the files after it. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
100060
102270
  readConfiguredBytes: readCodexcliProjectDocMaxBytes,
100061
102271
  configuredSource: "`project_doc_max_bytes` in `.codex/config.toml` (honored only if the project is trusted)",
100062
102272
  defaultLimitHint: "Rulesync does not read `~/.codex/config.toml`; ignore this warning if you already raised `project_doc_max_bytes` there."
@@ -100466,7 +102676,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
100466
102676
  extension: "md",
100467
102677
  supportsGlobal: true,
100468
102678
  ruleDiscoveryMode: "auto",
100469
- collisionPolicy: "fold"
102679
+ collisionPolicy: "fold",
102680
+ localRootMode: "separate-local-file",
102681
+ localRootFileName: "AGENTS.local.md"
100470
102682
  }
100471
102683
  }]
100472
102684
  ]);
@@ -100628,14 +102840,22 @@ var RulesProcessor = class extends FeatureProcessor {
100628
102840
  * estimate can come out low there. The budget checked is the value set in the
100629
102841
  * output root's tool config when the tool supports one (Codex's
100630
102842
  * `project_doc_max_bytes` in `.codex/config.toml`), else the tool's default;
100631
- * the warning names which one it used. Global scope is not
100632
- * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
100633
- * project budget.
102843
+ * the warning names which one it used. A configured budget of `0` disables
102844
+ * project docs in Codex, so that case gets a single warning saying the
102845
+ * generated files will not be loaded instead of a size report. Global scope
102846
+ * is not checked because Codex loads the personal `~/.codex/AGENTS.md`
102847
+ * outside the project budget.
100634
102848
  */
100635
102849
  async warnForInstructionBudget({ toolRules, meta }) {
100636
102850
  const budget = meta.projectInstructionBudget;
100637
- if (budget === void 0 || this.global) return;
102851
+ if (budget === void 0 || this.global || toolRules.length === 0) return;
100638
102852
  const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
102853
+ const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
102854
+ if (configuredBytes === 0) {
102855
+ const paths = toolRules.map((rule) => toPosixPath(join(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
102856
+ this.logger.warn(`${budget.configuredSource ?? "The tool config"}${location} is set to 0, which disables ${this.toolTarget}'s project instruction files: the generated ${paths.join(", ")} will not be loaded. Remove the setting or set it to a positive byte count to load them.`);
102857
+ return;
102858
+ }
100639
102859
  const limitBytes = configuredBytes ?? budget.bytes;
100640
102860
  const limitLabel = budget.readConfiguredBytes === void 0 ? `${limitBytes}-byte limit` : configuredBytes === void 0 ? `${limitBytes}-byte default limit` : `${limitBytes}-byte limit set by ${budget.configuredSource ?? "the tool config"}`;
100641
102861
  const remedy = configuredBytes === void 0 && budget.defaultLimitHint !== void 0 ? `${budget.remedy} ${budget.defaultLimitHint}` : budget.remedy;
@@ -100645,7 +102865,6 @@ var RulesProcessor = class extends FeatureProcessor {
100645
102865
  bytes: Buffer.byteLength(addTrailingNewline(rule.getFileContent()), "utf8")
100646
102866
  }));
100647
102867
  const isAncestorOrSelf = (ancestor, dir) => ancestor === "." || ancestor === dir || dir.startsWith(`${ancestor}/`);
100648
- const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
100649
102868
  const rootFiles = files.filter(({ dir }) => dir === ".");
100650
102869
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100651
102870
  if (rootBytes > limitBytes) {
@@ -100953,10 +103172,11 @@ var RulesProcessor = class extends FeatureProcessor {
100953
103172
  continue;
100954
103173
  }
100955
103174
  const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
100956
- const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
100957
103175
  const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
100958
- const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
100959
- const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
103176
+ const { shouldCompose, sharedRootFrontmatter } = decideRuleComposition({
103177
+ group,
103178
+ collisionPolicy
103179
+ });
100960
103180
  if (!shouldCompose && hasSourceRoot) throw new Error(`Multiple generated rules resolve to output path '${path}' for target '${this.toolTarget}', but this target cannot safely compose a collision involving a root rule. Source rules: ${formatRulePaths(group.map(({ rulesyncRule }) => rulesyncRule))}`);
100961
103181
  if (!shouldCompose) {
100962
103182
  for (const conversion of group) survivors.add(conversion);
@@ -100964,8 +103184,11 @@ var RulesProcessor = class extends FeatureProcessor {
100964
103184
  }
100965
103185
  const target = rootConversion ?? group[0];
100966
103186
  if (!target) continue;
100967
- const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
100968
- target.toolRule.setFileContent(mergedContent);
103187
+ const ordered = [target, ...group.filter((rule) => rule !== target)];
103188
+ target.toolRule.setFileContent(composeRuleFragments({
103189
+ contents: ordered.map(({ toolRule }) => toolRule.getFileContent()),
103190
+ sharedFrontmatter: sharedRootFrontmatter
103191
+ }));
100969
103192
  survivors.add(target);
100970
103193
  }
100971
103194
  for (let i = convertedRules.length - 1; i >= 0; i--) {
@@ -101049,7 +103272,7 @@ var RulesProcessor = class extends FeatureProcessor {
101049
103272
  /**
101050
103273
  * Handle localRoot rule generation based on tool target.
101051
103274
  * - `separate-local-file`: writes a dedicated `*.local.md` root file
101052
- * (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
103275
+ * (claudecode/legacy: `./CLAUDE.local.md`, rovodev/dsh: `./AGENTS.local.md`)
101053
103276
  * - `append-to-root` (default): appends the body to the root file
101054
103277
  */
101055
103278
  handleLocalRootRule(toolRules, localRootRule, factory) {
@@ -101165,7 +103388,11 @@ var RulesProcessor = class extends FeatureProcessor {
101165
103388
  localRoot
101166
103389
  });
101167
103390
  }
101168
- const workingRootClass = [CrushRule, ReasonixRule].find((base) => isClassOrSubclassOf({
103391
+ const workingRootClass = [
103392
+ CrushRule,
103393
+ ReasonixRule,
103394
+ DshRule
103395
+ ].find((base) => isClassOrSubclassOf({
101169
103396
  candidate: factory.class,
101170
103397
  base
101171
103398
  }));
@@ -104563,6 +106790,6 @@ async function importChecksCore(params) {
104563
106790
  return writtenCount;
104564
106791
  }
104565
106792
  //#endregion
104566
- export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, LETTACODE_DIR as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema 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, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames 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, mapWithConcurrency as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
106793
+ export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, CRUSH_LOCAL_RULE_FILE_NAME as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, LETTACODE_DIR as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema 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, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames 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, mapWithConcurrency as un, SKILL_FILE_NAME as ut, CommandsProcessor as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, REASONIX_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
104567
106794
 
104568
- //# sourceMappingURL=import-BsEFYJsg.js.map
106795
+ //# sourceMappingURL=import-CoFTCNq7.js.map