rulesync 29.0.0 → 31.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.cjs +17 -11
- package/dist/cli/index.js +17 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CLfSccAk.cjs → import-CBvXTNp8.cjs} +1457 -251
- package/dist/{import--SCvWeDn.js → import-ORYbMDJK.js} +1441 -235
- package/dist/{import--SCvWeDn.js.map → import-ORYbMDJK.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -913,6 +913,7 @@ const checksProcessorToolTargetTuple = [
|
|
|
913
913
|
"cursor",
|
|
914
914
|
"factorydroid",
|
|
915
915
|
"gitlabduo",
|
|
916
|
+
"goose",
|
|
916
917
|
"hermesagent",
|
|
917
918
|
"kilo",
|
|
918
919
|
"qwencode",
|
|
@@ -7861,6 +7862,26 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
|
|
|
7861
7862
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
7862
7863
|
defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
|
|
7863
7864
|
});
|
|
7865
|
+
/**
|
|
7866
|
+
* oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
|
|
7867
|
+
* before prompting. `always-ask` auto-approves only read-only tools, `write`
|
|
7868
|
+
* also workspace writes, and `yolo` (oh-my-pi's default) every tier.
|
|
7869
|
+
*
|
|
7870
|
+
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
|
|
7871
|
+
*/
|
|
7872
|
+
const OMP_APPROVAL_MODES = [
|
|
7873
|
+
"always-ask",
|
|
7874
|
+
"write",
|
|
7875
|
+
"yolo"
|
|
7876
|
+
];
|
|
7877
|
+
/**
|
|
7878
|
+
* Tool-scoped override block for oh-my-pi (`omp`). `approvalMode` writes the
|
|
7879
|
+
* `tools.approvalMode` key of `config.yml`, which has no canonical category.
|
|
7880
|
+
*/
|
|
7881
|
+
const OmpPermissionsOverrideSchema = z.looseObject({
|
|
7882
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
7883
|
+
approvalMode: z.optional(z.enum(OMP_APPROVAL_MODES))
|
|
7884
|
+
});
|
|
7864
7885
|
const KimiCodePermissionsOverrideSchema = z.looseObject({
|
|
7865
7886
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
7866
7887
|
defaultPermissionMode: z.optional(z.enum([
|
|
@@ -7868,6 +7889,14 @@ const KimiCodePermissionsOverrideSchema = z.looseObject({
|
|
|
7868
7889
|
"yolo",
|
|
7869
7890
|
"auto"
|
|
7870
7891
|
])),
|
|
7892
|
+
/**
|
|
7893
|
+
* Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
|
|
7894
|
+
* default `true`). `false` turns off the built-in dangerous-command policy
|
|
7895
|
+
* (no confirmation for `rm -rf`, `shutdown` and the like).
|
|
7896
|
+
*
|
|
7897
|
+
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
|
|
7898
|
+
*/
|
|
7899
|
+
dangerousCommandGuard: z.optional(z.boolean()),
|
|
7871
7900
|
rules: z.optional(z.array(z.looseObject({
|
|
7872
7901
|
decision: PermissionActionSchema,
|
|
7873
7902
|
pattern: z.string(),
|
|
@@ -8083,7 +8112,10 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
8083
8112
|
* `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
|
|
8084
8113
|
* `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
|
|
8085
8114
|
* `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
|
|
8086
|
-
* `allowedInsecureVoiceBaseUrls`)
|
|
8115
|
+
* `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
|
|
8116
|
+
* `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
|
|
8117
|
+
* Code unions across scopes) and `general` (`outputStyle`, the output style
|
|
8118
|
+
* that reshapes how responses are written). Qwen Code strips `tools.workflowsEnabled`,
|
|
8087
8119
|
* `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
|
|
8088
8120
|
* `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
|
|
8089
8121
|
* skips those four in project scope and announces a granting value in global
|
|
@@ -8100,7 +8132,8 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
8100
8132
|
* registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
|
|
8101
8133
|
* demotes an omitted tool to deferred rather than removing it, replacing rather
|
|
8102
8134
|
* than merging with the list a higher scope set), the Auto Mode
|
|
8103
|
-
* classifier config, and, because these groups are
|
|
8135
|
+
* classifier config, the skill kill-switches, and, because these groups are
|
|
8136
|
+
* loose objects, any key
|
|
8104
8137
|
* rulesync does not model. Import flags a scope-dependent key only when it read
|
|
8105
8138
|
* the project file, since that is the value a `--global` regenerate would
|
|
8106
8139
|
* promote. It also
|
|
@@ -8111,11 +8144,13 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
8111
8144
|
* matching `settings.json` group and emitted only for Qwen, while the shared
|
|
8112
8145
|
* `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
|
|
8113
8146
|
* arrays. Kept `looseObject` (verbatim passthrough) so any current or future
|
|
8114
|
-
* `tools`/`security`/`autoMode` key can be authored.
|
|
8147
|
+
* `tools`/`security`/`skills`/`general`/`autoMode` key can be authored.
|
|
8115
8148
|
*
|
|
8116
8149
|
* @example
|
|
8117
8150
|
* {
|
|
8118
8151
|
* "tools": { "approvalMode": "auto-edit" },
|
|
8152
|
+
* "skills": { "disabled": ["pdf"], "disabledLevels": ["bundled"] },
|
|
8153
|
+
* "general": { "outputStyle": "Concise" },
|
|
8119
8154
|
* "security": { "folderTrust": { "enabled": true } },
|
|
8120
8155
|
* "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
|
|
8121
8156
|
* }
|
|
@@ -8124,6 +8159,8 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
|
8124
8159
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
8125
8160
|
tools: z.optional(z.looseObject({})),
|
|
8126
8161
|
security: z.optional(z.looseObject({})),
|
|
8162
|
+
skills: z.optional(z.looseObject({})),
|
|
8163
|
+
general: z.optional(z.looseObject({})),
|
|
8127
8164
|
autoMode: z.optional(z.looseObject({}))
|
|
8128
8165
|
});
|
|
8129
8166
|
/**
|
|
@@ -8209,6 +8246,8 @@ const TabninePermissionsOverrideSchema = z.looseObject({
|
|
|
8209
8246
|
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
8210
8247
|
* @example
|
|
8211
8248
|
* { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
|
|
8249
|
+
* @example
|
|
8250
|
+
* { "mode": "deny" }
|
|
8212
8251
|
*/
|
|
8213
8252
|
const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
8214
8253
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
@@ -8217,7 +8256,12 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
8217
8256
|
rawAllow: z.optional(z.array(z.string())),
|
|
8218
8257
|
rawAsk: z.optional(z.array(z.string())),
|
|
8219
8258
|
rawDeny: z.optional(z.array(z.string())),
|
|
8220
|
-
allowDynamicBash: z.optional(z.boolean())
|
|
8259
|
+
allowDynamicBash: z.optional(z.boolean()),
|
|
8260
|
+
mode: z.optional(z.enum([
|
|
8261
|
+
"ask",
|
|
8262
|
+
"allow",
|
|
8263
|
+
"deny"
|
|
8264
|
+
]))
|
|
8221
8265
|
});
|
|
8222
8266
|
/**
|
|
8223
8267
|
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
@@ -8260,7 +8304,8 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({
|
|
|
8260
8304
|
* {
|
|
8261
8305
|
* "agent_mode_coding_permissions": "always_allow_reading",
|
|
8262
8306
|
* "agent_mode_execute_readonly_commands": true,
|
|
8263
|
-
* "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] }
|
|
8307
|
+
* "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] },
|
|
8308
|
+
* "auto_approve_bypasses_command_denylist": false
|
|
8264
8309
|
* }
|
|
8265
8310
|
*/
|
|
8266
8311
|
/**
|
|
@@ -8312,6 +8357,16 @@ const WarpExecutionProfileOverrideSchema = z.looseObject({
|
|
|
8312
8357
|
mcp_allowlist: z.optional(z.array(z.string())),
|
|
8313
8358
|
mcp_denylist: z.optional(z.array(z.string()))
|
|
8314
8359
|
});
|
|
8360
|
+
/**
|
|
8361
|
+
* Whether auto-approve may run commands that match the command denylist
|
|
8362
|
+
* (Warp defaults to `true`). It is not an execution-profile field: it lives in
|
|
8363
|
+
* the sibling `[agents.warp_agent.other]` table of the same `settings.toml`,
|
|
8364
|
+
* shared by the Warp app and the Warp Agent CLI.
|
|
8365
|
+
*
|
|
8366
|
+
* @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
|
|
8367
|
+
* @see https://github.com/warpdotdev/warp/blob/main/app/src/settings/ai.rs (`AutoApproveBypassesCommandDenylist`)
|
|
8368
|
+
*/
|
|
8369
|
+
const WarpAutoApproveBypassSchema = z.optional(z.boolean());
|
|
8315
8370
|
const WarpPermissionsOverrideSchema = z.looseObject({
|
|
8316
8371
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
8317
8372
|
agent_mode_coding_permissions: z.optional(z.enum([
|
|
@@ -8321,7 +8376,8 @@ const WarpPermissionsOverrideSchema = z.looseObject({
|
|
|
8321
8376
|
])),
|
|
8322
8377
|
agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
|
|
8323
8378
|
agent_mode_execute_readonly_commands: z.optional(z.boolean()),
|
|
8324
|
-
execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
|
|
8379
|
+
execution_profile: z.optional(WarpExecutionProfileOverrideSchema),
|
|
8380
|
+
auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
|
|
8325
8381
|
});
|
|
8326
8382
|
/**
|
|
8327
8383
|
* Tool-scoped override block for the standalone Warp Agent CLI. The CLI reads
|
|
@@ -8330,16 +8386,21 @@ const WarpPermissionsOverrideSchema = z.looseObject({
|
|
|
8330
8386
|
* the app's legacy `[agents.profiles]` keys), so only the nested
|
|
8331
8387
|
* `execution_profile` block is offered; it is merged into that record, and the
|
|
8332
8388
|
* shared `permission.bash` block still drives `command_allowlist` /
|
|
8333
|
-
* `command_denylist`.
|
|
8389
|
+
* `command_denylist`. `auto_approve_bypasses_command_denylist` is written to
|
|
8390
|
+
* the sibling `[agents.warp_agent.other]` table.
|
|
8334
8391
|
*
|
|
8335
8392
|
* @example
|
|
8336
|
-
* {
|
|
8393
|
+
* {
|
|
8394
|
+
* "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" },
|
|
8395
|
+
* "auto_approve_bypasses_command_denylist": false
|
|
8396
|
+
* }
|
|
8337
8397
|
*
|
|
8338
8398
|
* @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
|
|
8339
8399
|
*/
|
|
8340
8400
|
const WarpcliPermissionsOverrideSchema = z.looseObject({
|
|
8341
8401
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
8342
|
-
execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
|
|
8402
|
+
execution_profile: z.optional(WarpExecutionProfileOverrideSchema),
|
|
8403
|
+
auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
|
|
8343
8404
|
});
|
|
8344
8405
|
/**
|
|
8345
8406
|
* deepagents-cli's approval-mode knobs under `[startup]` in
|
|
@@ -9035,7 +9096,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
9035
9096
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9036
9097
|
crush: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9037
9098
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9038
|
-
omp: z.optional(
|
|
9099
|
+
omp: z.optional(OmpPermissionsOverrideSchema),
|
|
9039
9100
|
pool: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9040
9101
|
qoder: z.optional(CanonicalPermissionsOverrideSchema),
|
|
9041
9102
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -12745,7 +12806,9 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12745
12806
|
ownedKeys: [
|
|
12746
12807
|
"permissions",
|
|
12747
12808
|
"tools",
|
|
12748
|
-
"security"
|
|
12809
|
+
"security",
|
|
12810
|
+
"skills",
|
|
12811
|
+
"general"
|
|
12749
12812
|
]
|
|
12750
12813
|
}
|
|
12751
12814
|
}
|
|
@@ -13115,7 +13178,7 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13115
13178
|
features: {
|
|
13116
13179
|
mcp: {
|
|
13117
13180
|
kind: "replace-owned-keys",
|
|
13118
|
-
ownedKeys: ["mcp_servers"]
|
|
13181
|
+
ownedKeys: ["mcp_servers", "disabled_mcp_tools"]
|
|
13119
13182
|
},
|
|
13120
13183
|
permissions: {
|
|
13121
13184
|
kind: "replace-owned-keys",
|
|
@@ -13250,6 +13313,14 @@ function serializeSharedConfigFile({ fileKey, document, existingContent, filePat
|
|
|
13250
13313
|
});
|
|
13251
13314
|
}
|
|
13252
13315
|
/**
|
|
13316
|
+
* A patch value for an owned key that holds a map of managed entries: the map
|
|
13317
|
+
* itself, or `undefined` when it is empty, which retracts the key instead of
|
|
13318
|
+
* leaving an empty object behind (`applySharedConfigPatch` deletes a key whose patch value is `undefined`).
|
|
13319
|
+
*/
|
|
13320
|
+
function undefinedWhenEmpty(entries) {
|
|
13321
|
+
return Object.keys(entries).length > 0 ? entries : void 0;
|
|
13322
|
+
}
|
|
13323
|
+
/**
|
|
13253
13324
|
* Execute a feature's declared write to a gateway-managed shared file: parse
|
|
13254
13325
|
* the existing content, merge the patch under the feature's declared policy,
|
|
13255
13326
|
* and serialize it back over the existing content (see
|
|
@@ -13736,14 +13807,23 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
13736
13807
|
async removeOrphanAiFiles(existingFiles, generatedFiles) {
|
|
13737
13808
|
const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
|
|
13738
13809
|
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
|
|
13810
|
+
const refusedDirs = /* @__PURE__ */ new Map();
|
|
13739
13811
|
let removedCount = 0;
|
|
13740
13812
|
for (const aiFile of orphanFiles) {
|
|
13741
13813
|
const filePath = aiFile.getFilePath();
|
|
13742
|
-
|
|
13743
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13814
|
+
const rootPath = aiFile.getOutputRoot();
|
|
13815
|
+
const dirPath = dirname(filePath);
|
|
13816
|
+
const cacheKey = JSON.stringify([rootPath, dirPath]);
|
|
13817
|
+
let refused = refusedDirs.get(cacheKey);
|
|
13818
|
+
if (refused === void 0) {
|
|
13819
|
+
refused = await refusesDeleteOutsideRoot({
|
|
13820
|
+
logger: this.logger,
|
|
13821
|
+
rootPath,
|
|
13822
|
+
dirPath
|
|
13823
|
+
});
|
|
13824
|
+
refusedDirs.set(cacheKey, refused);
|
|
13825
|
+
}
|
|
13826
|
+
if (refused) continue;
|
|
13747
13827
|
const loggedPath = stripControlCharacters(filePath);
|
|
13748
13828
|
if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
|
|
13749
13829
|
else {
|
|
@@ -13864,22 +13944,24 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
|
13864
13944
|
return true;
|
|
13865
13945
|
}
|
|
13866
13946
|
/**
|
|
13867
|
-
* Whether deleting `
|
|
13868
|
-
* the
|
|
13947
|
+
* Whether deleting the files in `dirPath` has to be refused because a link on
|
|
13948
|
+
* the way to that directory leads out of `rootPath`, warning about the refusal
|
|
13949
|
+
* once for the whole directory.
|
|
13869
13950
|
*
|
|
13870
13951
|
* Deletion candidates are enumerated without following links *inside* the
|
|
13871
13952
|
* scanned directory, but the scanned directory itself is reached through
|
|
13872
13953
|
* whatever links its path contains: a checked-out repository can carry
|
|
13873
13954
|
* `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
|
|
13874
|
-
* an orphan. Only the
|
|
13875
|
-
* a link unlinks the link where it is spelled
|
|
13955
|
+
* an orphan. Only the directory is judged, never the file in it, because
|
|
13956
|
+
* removing a file that is itself a link unlinks the link where it is spelled
|
|
13957
|
+
* and never touches its target.
|
|
13876
13958
|
*/
|
|
13877
|
-
async function refusesDeleteOutsideRoot({ logger, rootPath,
|
|
13959
|
+
async function refusesDeleteOutsideRoot({ logger, rootPath, dirPath }) {
|
|
13878
13960
|
if (!await writablePathEscapesRoot({
|
|
13879
13961
|
rootPath,
|
|
13880
|
-
targetPath:
|
|
13962
|
+
targetPath: dirPath
|
|
13881
13963
|
})) return false;
|
|
13882
|
-
logger.warn(`Refusing to delete ${quoteForLog(
|
|
13964
|
+
logger.warn(`Refusing to delete files in ${quoteForLog(dirPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
|
|
13883
13965
|
return true;
|
|
13884
13966
|
}
|
|
13885
13967
|
/**
|
|
@@ -14118,12 +14200,12 @@ const AmpCheckFrontmatterSchema = z.looseObject({
|
|
|
14118
14200
|
* this format continue to apply.
|
|
14119
14201
|
* @see https://ampcode.com/docs/review
|
|
14120
14202
|
*/
|
|
14121
|
-
var AmpCheck = class
|
|
14203
|
+
var AmpCheck = class extends ToolCheck {
|
|
14122
14204
|
frontmatter;
|
|
14123
14205
|
body;
|
|
14124
14206
|
constructor({ frontmatter, body, fileContent, ...rest }) {
|
|
14125
14207
|
if (rest.validate !== false) {
|
|
14126
|
-
const result =
|
|
14208
|
+
const result = new.target.getFrontmatterSchema().safeParse(frontmatter);
|
|
14127
14209
|
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
14128
14210
|
}
|
|
14129
14211
|
super({
|
|
@@ -14133,6 +14215,26 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14133
14215
|
this.frontmatter = frontmatter;
|
|
14134
14216
|
this.body = body;
|
|
14135
14217
|
}
|
|
14218
|
+
/**
|
|
14219
|
+
* The tool target this check format belongs to, which is also the name of
|
|
14220
|
+
* its tool-specific frontmatter section. Goose reads the same per-file
|
|
14221
|
+
* format with a few extra keys, so `GooseCheck` reuses this class through
|
|
14222
|
+
* these hooks.
|
|
14223
|
+
*/
|
|
14224
|
+
static getCheckToolTarget() {
|
|
14225
|
+
return "amp";
|
|
14226
|
+
}
|
|
14227
|
+
/** The frontmatter schema a check file of this tool is validated against. */
|
|
14228
|
+
static getFrontmatterSchema() {
|
|
14229
|
+
return AmpCheckFrontmatterSchema;
|
|
14230
|
+
}
|
|
14231
|
+
/**
|
|
14232
|
+
* Frontmatter keys only this tool reads. Import moves them into the tool's
|
|
14233
|
+
* own section so they are not passed on to every other checks target.
|
|
14234
|
+
*/
|
|
14235
|
+
static getToolOnlyKeys() {
|
|
14236
|
+
return [];
|
|
14237
|
+
}
|
|
14136
14238
|
static getSettablePaths({ global = false } = {}) {
|
|
14137
14239
|
return { relativeDirPath: global ? AMP_CHECKS_GLOBAL_DIR : AMP_CHECKS_PROJECT_DIR };
|
|
14138
14240
|
}
|
|
@@ -14144,6 +14246,10 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14144
14246
|
}
|
|
14145
14247
|
toRulesyncCheck() {
|
|
14146
14248
|
const { name: _name, description, "severity-default": severityDefault, tools, ...restFields } = this.frontmatter;
|
|
14249
|
+
const checkClass = this.constructor;
|
|
14250
|
+
const toolOnlyKeys = new Set(checkClass.getToolOnlyKeys());
|
|
14251
|
+
const sharedFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolOnlyKeys.has(key)));
|
|
14252
|
+
const toolSection = Object.fromEntries(Object.entries(restFields).filter(([key]) => toolOnlyKeys.has(key)));
|
|
14147
14253
|
return new RulesyncCheck({
|
|
14148
14254
|
outputRoot: ".",
|
|
14149
14255
|
frontmatter: {
|
|
@@ -14151,7 +14257,8 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14151
14257
|
...description !== void 0 && { description },
|
|
14152
14258
|
...severityDefault !== void 0 && { severity: severityDefault },
|
|
14153
14259
|
...tools !== void 0 && { tools },
|
|
14154
|
-
...
|
|
14260
|
+
...sharedFields,
|
|
14261
|
+
...Object.keys(toolSection).length > 0 && { [checkClass.getCheckToolTarget()]: toolSection }
|
|
14155
14262
|
},
|
|
14156
14263
|
body: this.body,
|
|
14157
14264
|
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
@@ -14166,22 +14273,24 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14166
14273
|
const { targets: _targets, description, severity, tools, ...restFields } = rulesyncFrontmatter;
|
|
14167
14274
|
const toolTargetKeys = new Set(ALL_TOOL_TARGETS);
|
|
14168
14275
|
const passthroughFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolTargetKeys.has(key) && key !== "name"));
|
|
14169
|
-
const
|
|
14276
|
+
const toolTarget = this.getCheckToolTarget();
|
|
14277
|
+
const rawSection = rulesyncFrontmatter[toolTarget];
|
|
14278
|
+
const toolSection = this.filterToolSpecificSection(rawSection !== null && typeof rawSection === "object" && !Array.isArray(rawSection) ? rawSection : {}, ["name"]);
|
|
14170
14279
|
const rawAmpFrontmatter = {
|
|
14171
14280
|
name,
|
|
14172
14281
|
...description !== void 0 && { description },
|
|
14173
14282
|
...severity !== void 0 && { "severity-default": severity },
|
|
14174
14283
|
...tools !== void 0 && { tools },
|
|
14175
14284
|
...passthroughFields,
|
|
14176
|
-
...
|
|
14285
|
+
...toolSection
|
|
14177
14286
|
};
|
|
14178
|
-
const result =
|
|
14179
|
-
if (!result.success) throw new Error(`Invalid
|
|
14287
|
+
const result = this.getFrontmatterSchema().safeParse(rawAmpFrontmatter);
|
|
14288
|
+
if (!result.success) throw new Error(`Invalid ${toolTarget} check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
|
|
14180
14289
|
const ampFrontmatter = result.data;
|
|
14181
14290
|
const body = rulesyncCheck.getBody();
|
|
14182
14291
|
const fileContent = stringifyFrontmatter(body, ampFrontmatter);
|
|
14183
14292
|
const paths = this.getSettablePaths({ global });
|
|
14184
|
-
return new
|
|
14293
|
+
return new this({
|
|
14185
14294
|
outputRoot,
|
|
14186
14295
|
frontmatter: ampFrontmatter,
|
|
14187
14296
|
body,
|
|
@@ -14196,7 +14305,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14196
14305
|
success: true,
|
|
14197
14306
|
error: null
|
|
14198
14307
|
};
|
|
14199
|
-
const result =
|
|
14308
|
+
const result = this.constructor.getFrontmatterSchema().safeParse(this.frontmatter);
|
|
14200
14309
|
if (result.success) return {
|
|
14201
14310
|
success: true,
|
|
14202
14311
|
error: null
|
|
@@ -14209,7 +14318,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14209
14318
|
static isTargetedByRulesyncCheck(rulesyncCheck) {
|
|
14210
14319
|
return this.isTargetedByRulesyncCheckDefault({
|
|
14211
14320
|
rulesyncCheck,
|
|
14212
|
-
toolTarget:
|
|
14321
|
+
toolTarget: this.getCheckToolTarget()
|
|
14213
14322
|
});
|
|
14214
14323
|
}
|
|
14215
14324
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
@@ -14217,9 +14326,9 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14217
14326
|
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
14218
14327
|
const fileContent = await readFileContent(filePath);
|
|
14219
14328
|
const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
|
|
14220
|
-
const result =
|
|
14329
|
+
const result = this.getFrontmatterSchema().safeParse(frontmatter);
|
|
14221
14330
|
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
14222
|
-
return new
|
|
14331
|
+
return new this({
|
|
14223
14332
|
outputRoot,
|
|
14224
14333
|
relativeDirPath: paths.relativeDirPath,
|
|
14225
14334
|
relativeFilePath,
|
|
@@ -14230,7 +14339,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
|
|
|
14230
14339
|
});
|
|
14231
14340
|
}
|
|
14232
14341
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
14233
|
-
return new
|
|
14342
|
+
return new this({
|
|
14234
14343
|
outputRoot,
|
|
14235
14344
|
relativeDirPath,
|
|
14236
14345
|
relativeFilePath,
|
|
@@ -15330,6 +15439,60 @@ var GitlabduoCheck = class GitlabduoCheck extends ToolCheck {
|
|
|
15330
15439
|
}
|
|
15331
15440
|
};
|
|
15332
15441
|
//#endregion
|
|
15442
|
+
//#region src/constants/goose-paths.ts
|
|
15443
|
+
const GOOSE_DIR = ".goose";
|
|
15444
|
+
const GOOSE_GLOBAL_DIR = join(".config", "goose");
|
|
15445
|
+
const GOOSE_RULE_FILE_NAME = ".goosehints";
|
|
15446
|
+
const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
15447
|
+
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
15448
|
+
const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
|
|
15449
|
+
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
15450
|
+
const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
|
|
15451
|
+
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
15452
|
+
const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
|
|
15453
|
+
const GOOSE_GLOBAL_SKILLS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "skills");
|
|
15454
|
+
const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
|
|
15455
|
+
const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
|
|
15456
|
+
const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
|
|
15457
|
+
const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
|
|
15458
|
+
const GOOSE_CHECKS_PROJECT_DIR = join(".agents", "checks");
|
|
15459
|
+
const GOOSE_CHECKS_GLOBAL_DIR = join(GOOSE_GLOBAL_DIR, "checks");
|
|
15460
|
+
//#endregion
|
|
15461
|
+
//#region src/features/checks/goose-check.ts
|
|
15462
|
+
const GooseCheckFrontmatterSchema = z.extend(AmpCheckFrontmatterSchema, {
|
|
15463
|
+
model: z.optional(z.string()),
|
|
15464
|
+
"turn-limit": z.optional(z.int().check(z.nonnegative()))
|
|
15465
|
+
});
|
|
15466
|
+
/**
|
|
15467
|
+
* Represents a Goose review check (`goose review`), written to the user-wide
|
|
15468
|
+
* `~/.config/goose/checks/` (global scope only).
|
|
15469
|
+
*
|
|
15470
|
+
* Goose discovers repository checks in `.agents/checks/*.md`, the same
|
|
15471
|
+
* cross-tool directory the `amp` checks target already writes (Goose's
|
|
15472
|
+
* `tools` and `severity-default` fields exist for parity with Amp's), so the
|
|
15473
|
+
* project scope is left to `amp` rather than having two targets own the same
|
|
15474
|
+
* files. Its global discovery reads `~/.config/goose/checks/` first, then
|
|
15475
|
+
* `~/.config/agents/checks/`. The file format is Amp's, so this class reuses
|
|
15476
|
+
* `AmpCheck` and adds Goose's `model` and `turn-limit`, which live in the
|
|
15477
|
+
* `goose:` section of a rulesync check; `name` is derived from the file name.
|
|
15478
|
+
*
|
|
15479
|
+
* @see https://github.com/aaif-goose/goose/blob/main/crates/goose/src/checks/mod.rs
|
|
15480
|
+
*/
|
|
15481
|
+
var GooseCheck = class extends AmpCheck {
|
|
15482
|
+
static getCheckToolTarget() {
|
|
15483
|
+
return "goose";
|
|
15484
|
+
}
|
|
15485
|
+
static getFrontmatterSchema() {
|
|
15486
|
+
return GooseCheckFrontmatterSchema;
|
|
15487
|
+
}
|
|
15488
|
+
static getToolOnlyKeys() {
|
|
15489
|
+
return ["model", "turn-limit"];
|
|
15490
|
+
}
|
|
15491
|
+
static getSettablePaths({ global = false } = {}) {
|
|
15492
|
+
return { relativeDirPath: global ? GOOSE_CHECKS_GLOBAL_DIR : GOOSE_CHECKS_PROJECT_DIR };
|
|
15493
|
+
}
|
|
15494
|
+
};
|
|
15495
|
+
//#endregion
|
|
15333
15496
|
//#region src/constants/hermesagent-paths.ts
|
|
15334
15497
|
/**
|
|
15335
15498
|
* Hermes Agent configuration-layout conventions.
|
|
@@ -16058,6 +16221,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
|
|
|
16058
16221
|
committedOutput: true
|
|
16059
16222
|
}
|
|
16060
16223
|
}],
|
|
16224
|
+
["goose", {
|
|
16225
|
+
class: GooseCheck,
|
|
16226
|
+
meta: {
|
|
16227
|
+
supportsGlobal: true,
|
|
16228
|
+
supportsProject: false,
|
|
16229
|
+
filePattern: "*.md"
|
|
16230
|
+
}
|
|
16231
|
+
}],
|
|
16061
16232
|
["hermesagent", {
|
|
16062
16233
|
class: HermesagentCheck,
|
|
16063
16234
|
meta: {
|
|
@@ -16103,7 +16274,9 @@ const defaultGetFactory$6 = (target) => {
|
|
|
16103
16274
|
return factory;
|
|
16104
16275
|
};
|
|
16105
16276
|
const allToolTargetKeys$5 = [...toolCheckFactories.keys()];
|
|
16106
|
-
const checksProcessorToolTargets = allToolTargetKeys$5
|
|
16277
|
+
const checksProcessorToolTargets = allToolTargetKeys$5.filter((target) => {
|
|
16278
|
+
return toolCheckFactories.get(target)?.meta.supportsProject !== false;
|
|
16279
|
+
});
|
|
16107
16280
|
const checksProcessorToolTargetsGlobal = allToolTargetKeys$5.filter((target) => {
|
|
16108
16281
|
return toolCheckFactories.get(target)?.meta.supportsGlobal ?? false;
|
|
16109
16282
|
});
|
|
@@ -16274,23 +16447,6 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
16274
16447
|
}
|
|
16275
16448
|
};
|
|
16276
16449
|
//#endregion
|
|
16277
|
-
//#region src/constants/goose-paths.ts
|
|
16278
|
-
const GOOSE_DIR = ".goose";
|
|
16279
|
-
const GOOSE_GLOBAL_DIR = join(".config", "goose");
|
|
16280
|
-
const GOOSE_RULE_FILE_NAME = ".goosehints";
|
|
16281
|
-
const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
16282
|
-
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
16283
|
-
const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
|
|
16284
|
-
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
16285
|
-
const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
|
|
16286
|
-
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
16287
|
-
const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
|
|
16288
|
-
const GOOSE_GLOBAL_SKILLS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "skills");
|
|
16289
|
-
const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
|
|
16290
|
-
const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
|
|
16291
|
-
const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
|
|
16292
|
-
const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
|
|
16293
|
-
//#endregion
|
|
16294
16450
|
//#region src/utils/tool-home.ts
|
|
16295
16451
|
/**
|
|
16296
16452
|
* Where the rulesync-side source files of a tool with a home override belong.
|
|
@@ -17202,7 +17358,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
17202
17358
|
static getProjectRelativeDirPath() {
|
|
17203
17359
|
throw new Error("Please implement this method in the subclass.");
|
|
17204
17360
|
}
|
|
17205
|
-
/** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/
|
|
17361
|
+
/** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/config/workflows`). */
|
|
17206
17362
|
static getGlobalRelativeDirPath() {
|
|
17207
17363
|
throw new Error("Please implement this method in the subclass.");
|
|
17208
17364
|
}
|
|
@@ -17365,7 +17521,8 @@ const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
|
|
|
17365
17521
|
const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
17366
17522
|
const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
|
|
17367
17523
|
const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = join(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
|
|
17368
|
-
const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR,
|
|
17524
|
+
const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "workflows");
|
|
17525
|
+
const ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
|
|
17369
17526
|
const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
|
|
17370
17527
|
const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
|
|
17371
17528
|
//#endregion
|
|
@@ -17395,10 +17552,12 @@ var AntigravityIdeSkill = class extends AntigravitySharedSkill {
|
|
|
17395
17552
|
* Emits each command as a skill in `.agents/skills/<name>/SKILL.md` (project
|
|
17396
17553
|
* scope) and `~/.gemini/config/skills/<name>/SKILL.md` (global scope), the
|
|
17397
17554
|
* IDE's skills directories. Legacy workflows are still imported from
|
|
17398
|
-
* `.agents/workflows/` and `~/.gemini/
|
|
17399
|
-
*
|
|
17555
|
+
* `.agents/workflows/` and `~/.gemini/config/workflows/` (the global workflows
|
|
17556
|
+
* directory of Antigravity 2.0), and, in global scope only, from the pre-2.0
|
|
17557
|
+
* `~/.gemini/antigravity/global_workflows/`. All body and frontmatter handling
|
|
17558
|
+
* is shared with {@link AntigravitySharedCommand}.
|
|
17400
17559
|
*/
|
|
17401
|
-
var AntigravityIdeCommand = class extends AntigravitySharedCommand {
|
|
17560
|
+
var AntigravityIdeCommand = class AntigravityIdeCommand extends AntigravitySharedCommand {
|
|
17402
17561
|
static getProjectRelativeDirPath() {
|
|
17403
17562
|
return ANTIGRAVITY_IDE_COMMANDS_DIR_PATH;
|
|
17404
17563
|
}
|
|
@@ -17408,6 +17567,37 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
|
|
|
17408
17567
|
static getSkillsRelativeDirPath({ global }) {
|
|
17409
17568
|
return AntigravityIdeSkill.getSettablePaths({ global }).relativeDirPath;
|
|
17410
17569
|
}
|
|
17570
|
+
/**
|
|
17571
|
+
* Import-only, global scope: the workflows left in the pre-2.0
|
|
17572
|
+
* `~/.gemini/antigravity/global_workflows/` directory, where rulesync also
|
|
17573
|
+
* wrote IDE global commands before they were emitted as skills. The
|
|
17574
|
+
* directory is never swept by `generate --delete`; a workflow with the same
|
|
17575
|
+
* file name in `~/.gemini/config/workflows/` takes precedence.
|
|
17576
|
+
*
|
|
17577
|
+
* @see https://antigravity.google/docs/migration/workflows-to-skills
|
|
17578
|
+
*/
|
|
17579
|
+
static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false, logger } = {}) {
|
|
17580
|
+
if (!global) return [];
|
|
17581
|
+
const filePaths = await findFilesByGlobs("*.md", { cwd: join(outputRoot, ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH) });
|
|
17582
|
+
return (await Promise.all(filePaths.map(async (filePath) => {
|
|
17583
|
+
const fileContent = await readFileContent(filePath);
|
|
17584
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
17585
|
+
const result = AntigravityCommandFrontmatterSchema.safeParse(frontmatter);
|
|
17586
|
+
if (!result.success) {
|
|
17587
|
+
logger?.warn(`Skipping ${filePath}: invalid frontmatter (${formatError(result.error)}).`);
|
|
17588
|
+
return;
|
|
17589
|
+
}
|
|
17590
|
+
return new AntigravityIdeCommand({
|
|
17591
|
+
outputRoot,
|
|
17592
|
+
relativeDirPath: ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH,
|
|
17593
|
+
relativeFilePath: basename(filePath),
|
|
17594
|
+
frontmatter: result.data,
|
|
17595
|
+
body: body.trim(),
|
|
17596
|
+
fileContent,
|
|
17597
|
+
global
|
|
17598
|
+
});
|
|
17599
|
+
}))).filter((command) => command !== void 0);
|
|
17600
|
+
}
|
|
17411
17601
|
getToolTargetName() {
|
|
17412
17602
|
return "antigravity-ide";
|
|
17413
17603
|
}
|
|
@@ -17978,27 +18168,71 @@ const CLINE_HOOKS_GLOBAL_DIR_PATH = join("Documents", "Cline", "Hooks");
|
|
|
17978
18168
|
const CLINE_HOOKS_MANIFEST_FILE_NAME = "rulesync-hooks.json";
|
|
17979
18169
|
//#endregion
|
|
17980
18170
|
//#region src/features/commands/cline-command.ts
|
|
18171
|
+
const CLINE_WORKFLOW_FIELD_TYPES = {
|
|
18172
|
+
name: "string",
|
|
18173
|
+
disabled: "boolean",
|
|
18174
|
+
enabled: "boolean"
|
|
18175
|
+
};
|
|
18176
|
+
function sanitizeClineWorkflowFrontmatter({ cline, relativeFilePath, logger }) {
|
|
18177
|
+
if (!isRecord$1(cline)) return {};
|
|
18178
|
+
return Object.fromEntries(Object.entries(cline).filter(([key, value]) => {
|
|
18179
|
+
const expected = Object.hasOwn(CLINE_WORKFLOW_FIELD_TYPES, key) ? CLINE_WORKFLOW_FIELD_TYPES[key] : void 0;
|
|
18180
|
+
if (expected === void 0 || typeof value === expected) return true;
|
|
18181
|
+
warnWithFallback(logger, `Cline commands: dropping cline.${key} from ${relativeFilePath}: Cline rejects the whole workflow unless it is a ${expected}.`);
|
|
18182
|
+
return false;
|
|
18183
|
+
}));
|
|
18184
|
+
}
|
|
18185
|
+
/**
|
|
18186
|
+
* Cline workflows (commands). A workflow may carry YAML frontmatter that Cline
|
|
18187
|
+
* reads: `name` (the slash-command name) and a `disabled` / `enabled` switch
|
|
18188
|
+
* (`parseWorkflowConfigFromMarkdown` in Cline's
|
|
18189
|
+
* `user-instruction-config-loader.ts`). The `cline:` section of a
|
|
18190
|
+
* `.rulesync/commands/*.md` file is written as that frontmatter, and every key
|
|
18191
|
+
* of an imported workflow's frontmatter is lifted back into `cline:`, so
|
|
18192
|
+
* hand-written workflow frontmatter survives an import → generate round trip.
|
|
18193
|
+
* Cline's own UI toggle is kept in Cline's state, not in the file. A workflow
|
|
18194
|
+
* without a `cline:` section is written as a bare body, as before.
|
|
18195
|
+
*
|
|
18196
|
+
* @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/extensions/config/user-instruction-config-loader.ts
|
|
18197
|
+
*/
|
|
17981
18198
|
var ClineCommand = class ClineCommand extends ToolCommand {
|
|
17982
18199
|
static getSettablePaths({ global } = {}) {
|
|
17983
18200
|
if (global) return { relativeDirPath: CLINE_COMMANDS_GLOBAL_DIR_PATH };
|
|
17984
18201
|
return { relativeDirPath: CLINE_COMMANDS_DIR_PATH };
|
|
17985
18202
|
}
|
|
18203
|
+
/** The workflow's own frontmatter, or an empty object for a bare body. */
|
|
18204
|
+
getClineFrontmatter() {
|
|
18205
|
+
const { frontmatter } = parseFrontmatter(this.getFileContent(), this.getFilePath());
|
|
18206
|
+
return isRecord$1(frontmatter) ? frontmatter : {};
|
|
18207
|
+
}
|
|
17986
18208
|
toRulesyncCommand() {
|
|
18209
|
+
const cline = this.getClineFrontmatter();
|
|
18210
|
+
const rulesyncFrontmatter = {
|
|
18211
|
+
targets: ["*"],
|
|
18212
|
+
...Object.keys(cline).length > 0 && { cline }
|
|
18213
|
+
};
|
|
18214
|
+
const body = this.getBody();
|
|
17987
18215
|
return new RulesyncCommand({
|
|
17988
18216
|
outputRoot: process.cwd(),
|
|
17989
|
-
frontmatter:
|
|
17990
|
-
body
|
|
18217
|
+
frontmatter: rulesyncFrontmatter,
|
|
18218
|
+
body,
|
|
17991
18219
|
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
17992
18220
|
relativeFilePath: this.relativeFilePath,
|
|
17993
|
-
fileContent:
|
|
18221
|
+
fileContent: stringifyFrontmatter(body, rulesyncFrontmatter),
|
|
17994
18222
|
validate: true
|
|
17995
18223
|
});
|
|
17996
18224
|
}
|
|
17997
|
-
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
18225
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
|
|
17998
18226
|
const paths = this.getSettablePaths({ global });
|
|
18227
|
+
const cline = sanitizeClineWorkflowFrontmatter({
|
|
18228
|
+
cline: rulesyncCommand.getFrontmatter().cline,
|
|
18229
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
18230
|
+
logger
|
|
18231
|
+
});
|
|
18232
|
+
const body = rulesyncCommand.getBody();
|
|
17999
18233
|
return new ClineCommand({
|
|
18000
18234
|
outputRoot,
|
|
18001
|
-
fileContent:
|
|
18235
|
+
fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body, cline) : body,
|
|
18002
18236
|
relativeDirPath: paths.relativeDirPath,
|
|
18003
18237
|
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
18004
18238
|
validate
|
|
@@ -18011,7 +18245,7 @@ var ClineCommand = class ClineCommand extends ToolCommand {
|
|
|
18011
18245
|
};
|
|
18012
18246
|
}
|
|
18013
18247
|
getBody() {
|
|
18014
|
-
return this.getFileContent();
|
|
18248
|
+
return parseFrontmatter(this.getFileContent(), this.getFilePath()).body.trim();
|
|
18015
18249
|
}
|
|
18016
18250
|
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
18017
18251
|
return this.isTargetedByRulesyncCommandDefault({
|
|
@@ -18022,12 +18256,13 @@ var ClineCommand = class ClineCommand extends ToolCommand {
|
|
|
18022
18256
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
18023
18257
|
const paths = this.getSettablePaths({ global });
|
|
18024
18258
|
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
18025
|
-
const { body
|
|
18259
|
+
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
18260
|
+
const cline = isRecord$1(frontmatter) ? frontmatter : {};
|
|
18026
18261
|
return new ClineCommand({
|
|
18027
18262
|
outputRoot,
|
|
18028
18263
|
relativeDirPath: paths.relativeDirPath,
|
|
18029
18264
|
relativeFilePath,
|
|
18030
|
-
fileContent:
|
|
18265
|
+
fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body.trim(), cline) : body.trim(),
|
|
18031
18266
|
validate
|
|
18032
18267
|
});
|
|
18033
18268
|
}
|
|
@@ -25060,7 +25295,7 @@ function findCommandPaths({ command, scan }) {
|
|
|
25060
25295
|
} else {
|
|
25061
25296
|
if (path !== void 0) toAnchor.add(path);
|
|
25062
25297
|
state.owner = text.slice(text.lastIndexOf("/") + 1);
|
|
25063
|
-
state.expecting = afterCommandWord(state.owner);
|
|
25298
|
+
state.expecting = afterCommandWord$1(state.owner);
|
|
25064
25299
|
state.prefix = void 0;
|
|
25065
25300
|
state.shellCode = false;
|
|
25066
25301
|
}
|
|
@@ -25126,7 +25361,7 @@ function readOption({ state, text }) {
|
|
|
25126
25361
|
if (SHELL_NAME_PATTERN.test(state.owner) && SHELL_CODE_OPTION.test(text)) state.shellCode = true;
|
|
25127
25362
|
}
|
|
25128
25363
|
/** What follows a command word whose basename is `name`. */
|
|
25129
|
-
function afterCommandWord(name) {
|
|
25364
|
+
function afterCommandWord$1(name) {
|
|
25130
25365
|
if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
|
|
25131
25366
|
if (PACKAGE_RUNNERS.has(name)) return "wrapped";
|
|
25132
25367
|
return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
|
|
@@ -33469,6 +33704,19 @@ function isQwencodeLegacyMillisecondTimeout(timeout) {
|
|
|
33469
33704
|
return typeof timeout === "number" && Number.isFinite(timeout) && timeout >= QWENCODE_LEGACY_MILLISECOND_TIMEOUT_THRESHOLD;
|
|
33470
33705
|
}
|
|
33471
33706
|
/**
|
|
33707
|
+
* Read a `timeout` the way Qwen Code does. Settings files are not type-checked
|
|
33708
|
+
* and Qwen Code coerces a numeric string such as `"60000"` with `Number()`, so
|
|
33709
|
+
* import accepts one too; a value that is not a finite number is dropped, since
|
|
33710
|
+
* Qwen Code falls back to the hook type's default for it.
|
|
33711
|
+
* https://github.com/QwenLM/qwen-code/blob/main/packages/core/src/hooks/hook-timeout.ts
|
|
33712
|
+
*/
|
|
33713
|
+
function readQwencodeTimeout(timeout) {
|
|
33714
|
+
if (timeout === void 0) return void 0;
|
|
33715
|
+
if (typeof timeout === "string" && timeout.trim() === "") return void 0;
|
|
33716
|
+
const value = Number(timeout);
|
|
33717
|
+
return Number.isFinite(value) ? value : void 0;
|
|
33718
|
+
}
|
|
33719
|
+
/**
|
|
33472
33720
|
* Canonical `timeout` is seconds. A command hook value below the threshold is
|
|
33473
33721
|
* already read as seconds; a larger one can only be expressed in milliseconds.
|
|
33474
33722
|
*/
|
|
@@ -33579,7 +33827,7 @@ const QwencodeHookEntrySchema = z.looseObject({
|
|
|
33579
33827
|
type: z.optional(z.string()),
|
|
33580
33828
|
command: z.optional(z.string()),
|
|
33581
33829
|
url: z.optional(z.string()),
|
|
33582
|
-
timeout: z.optional(z.number()),
|
|
33830
|
+
timeout: z.optional(z.union([z.number(), z.string()])),
|
|
33583
33831
|
name: z.optional(z.string()),
|
|
33584
33832
|
description: z.optional(z.string()),
|
|
33585
33833
|
statusMessage: z.optional(z.string()),
|
|
@@ -33612,12 +33860,13 @@ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
|
|
|
33612
33860
|
const isCommand = hookType === "command";
|
|
33613
33861
|
const isPrompt = hookType === "prompt";
|
|
33614
33862
|
const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
|
|
33863
|
+
const timeout = readQwencodeTimeout(h.timeout);
|
|
33615
33864
|
return {
|
|
33616
33865
|
type: hookType,
|
|
33617
33866
|
...compact({
|
|
33618
33867
|
command: h.command,
|
|
33619
33868
|
url: h.url,
|
|
33620
|
-
timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(
|
|
33869
|
+
timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(timeout) : timeout,
|
|
33621
33870
|
name: h.name,
|
|
33622
33871
|
description: h.description,
|
|
33623
33872
|
statusMessage: h.statusMessage,
|
|
@@ -38119,7 +38368,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
38119
38368
|
fileKey: sharedConfigFileKey(basePaths),
|
|
38120
38369
|
feature: "mcp",
|
|
38121
38370
|
existingContent: fileContent ?? "",
|
|
38122
|
-
patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
|
|
38371
|
+
patch: { [AMP_MCP_SERVERS_KEY]: undefinedWhenEmpty(filterMcpServers(rulesyncMcp.getMcpServers())) },
|
|
38123
38372
|
filePath: join(jsonDir, relativeFilePath),
|
|
38124
38373
|
logger
|
|
38125
38374
|
}),
|
|
@@ -38417,6 +38666,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
|
38417
38666
|
const paths = this.getSettablePaths({ global });
|
|
38418
38667
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
38419
38668
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
38669
|
+
const mcpServers = rulesyncMcp.getMcpServers();
|
|
38420
38670
|
return new AugmentcodeMcp({
|
|
38421
38671
|
outputRoot,
|
|
38422
38672
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -38425,7 +38675,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
|
38425
38675
|
fileKey: sharedConfigFileKey(paths),
|
|
38426
38676
|
feature: "mcp",
|
|
38427
38677
|
existingContent,
|
|
38428
|
-
patch: { mcpServers:
|
|
38678
|
+
patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
|
|
38429
38679
|
filePath,
|
|
38430
38680
|
logger
|
|
38431
38681
|
}),
|
|
@@ -38981,6 +39231,229 @@ var ClaudecodeMcp = class ClaudecodeMcp extends ToolMcp {
|
|
|
38981
39231
|
}
|
|
38982
39232
|
};
|
|
38983
39233
|
//#endregion
|
|
39234
|
+
//#region src/features/mcp/claudecode-plugin-mcp.ts
|
|
39235
|
+
/**
|
|
39236
|
+
* The placeholder Claude Code substitutes with the installed plugin's own
|
|
39237
|
+
* directory in an MCP server's `command`, `args` and `env`.
|
|
39238
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
39239
|
+
*/
|
|
39240
|
+
const PLUGIN_ROOT_PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}";
|
|
39241
|
+
const PLUGIN_ROOT_PREFIX = /^(?:\$\{CLAUDE_PLUGIN_ROOT\}|"\$CLAUDE_PLUGIN_ROOT"|\$CLAUDE_PLUGIN_ROOT)\//;
|
|
39242
|
+
/**
|
|
39243
|
+
* Whether a server `command` names a file inside the plugin rather than an
|
|
39244
|
+
* executable found on `PATH`. The command is spawned without a shell, so a
|
|
39245
|
+
* name with a slash is a path, and one without (`npx`, `node`, `uvx`) is a
|
|
39246
|
+
* `PATH` lookup. An absolute path, a home path, one that leaves the plugin
|
|
39247
|
+
* (`../`), and one that already starts with a variable are left as written.
|
|
39248
|
+
*/
|
|
39249
|
+
function isPluginRelativeCommand(command) {
|
|
39250
|
+
return /[\\/]/.test(command) && !command.startsWith("$") && !command.startsWith("%") && !command.startsWith("~") && !/^\.\.[\\/]/.test(command) && !posix.isAbsolute(command) && !win32.isAbsolute(command);
|
|
39251
|
+
}
|
|
39252
|
+
function anchorToPluginRoot(path) {
|
|
39253
|
+
return `${PLUGIN_ROOT_PLACEHOLDER}/${path.replace(/^\.[\\/]/, "")}`;
|
|
39254
|
+
}
|
|
39255
|
+
/** The runner name of a command word: its basename without a `.exe` suffix. */
|
|
39256
|
+
function runnerName(command) {
|
|
39257
|
+
return (command.split(/[\\/]/).pop() ?? command).replace(/\.exe$/i, "");
|
|
39258
|
+
}
|
|
39259
|
+
/** What follows a command word, as plugin hook commands read it. */
|
|
39260
|
+
function afterCommandWord(name) {
|
|
39261
|
+
if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
|
|
39262
|
+
if (PACKAGE_RUNNERS.has(name)) return "command";
|
|
39263
|
+
return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
|
|
39264
|
+
}
|
|
39265
|
+
/**
|
|
39266
|
+
* The positions in `args` that name a file the server runs, read with the
|
|
39267
|
+
* rules plugin hook commands follow (`hook-command-paths.ts`): the script of a
|
|
39268
|
+
* script runner (`node ./dist/server.js`), the command after a run subcommand
|
|
39269
|
+
* or package runner (`uv run ./server.py`, `npx tsx ./x.ts`), and the value of
|
|
39270
|
+
* an option that loads a file (`node -r ./register.js`). Only a position whose
|
|
39271
|
+
* value `isPath` accepts is returned. Everything else is data the server reads
|
|
39272
|
+
* relative to the consumer's project (`server-filesystem ./docs`), and the
|
|
39273
|
+
* scan stops at an option that runs inline code or standard input
|
|
39274
|
+
* (`node -e`, `python3 -m`, `sh -c`, `bash -s`).
|
|
39275
|
+
*/
|
|
39276
|
+
function fileArgumentIndices({ command, args, isPath }) {
|
|
39277
|
+
const indices = /* @__PURE__ */ new Set();
|
|
39278
|
+
let owner = runnerName(command);
|
|
39279
|
+
let expecting = afterCommandWord(owner);
|
|
39280
|
+
for (let index = 0; index < args.length && expecting !== "none"; index++) {
|
|
39281
|
+
const arg = args[index];
|
|
39282
|
+
if (typeof arg !== "string") break;
|
|
39283
|
+
if (arg.startsWith("-") && arg !== "-") {
|
|
39284
|
+
if (optionsFor({
|
|
39285
|
+
table: CODE_OPTIONS,
|
|
39286
|
+
name: owner
|
|
39287
|
+
})?.has(arg) || SHELL_NAME_PATTERN.test(owner) && SHELL_CODE_OPTION.test(arg)) break;
|
|
39288
|
+
if (optionsFor({
|
|
39289
|
+
table: VALUE_OPTIONS,
|
|
39290
|
+
name: owner
|
|
39291
|
+
})?.has(arg)) {
|
|
39292
|
+
const value = args[index + 1];
|
|
39293
|
+
if (typeof value === "string" && isPath(value)) indices.add(index + 1);
|
|
39294
|
+
index++;
|
|
39295
|
+
}
|
|
39296
|
+
continue;
|
|
39297
|
+
}
|
|
39298
|
+
if (expecting === "subcommand") {
|
|
39299
|
+
if (RUN_SUBCOMMANDS[owner]?.has(arg)) {
|
|
39300
|
+
expecting = "command";
|
|
39301
|
+
continue;
|
|
39302
|
+
}
|
|
39303
|
+
if (!SCRIPT_RUNNER_PATTERN.test(owner)) break;
|
|
39304
|
+
expecting = "script";
|
|
39305
|
+
}
|
|
39306
|
+
if (isPath(arg)) indices.add(index);
|
|
39307
|
+
if (expecting === "script") break;
|
|
39308
|
+
owner = runnerName(arg);
|
|
39309
|
+
expecting = afterCommandWord(owner);
|
|
39310
|
+
}
|
|
39311
|
+
return indices;
|
|
39312
|
+
}
|
|
39313
|
+
function isDotRelative(value) {
|
|
39314
|
+
return /^\.[\\/]/.test(value);
|
|
39315
|
+
}
|
|
39316
|
+
/** Turn a plugin-root path back into its `./` form, or return it unchanged. */
|
|
39317
|
+
function relativizeFromPluginRoot(value) {
|
|
39318
|
+
return PLUGIN_ROOT_PREFIX.test(value) ? value.replace(PLUGIN_ROOT_PREFIX, "./") : value;
|
|
39319
|
+
}
|
|
39320
|
+
/** The command and arguments of a stdio server, with inline array arguments split out. */
|
|
39321
|
+
function splitCommand(server) {
|
|
39322
|
+
if (Array.isArray(server.command)) {
|
|
39323
|
+
const [first, ...rest] = server.command;
|
|
39324
|
+
return {
|
|
39325
|
+
command: typeof first === "string" ? first : void 0,
|
|
39326
|
+
args: [...rest, ...Array.isArray(server.args) ? server.args : []]
|
|
39327
|
+
};
|
|
39328
|
+
}
|
|
39329
|
+
return {
|
|
39330
|
+
command: typeof server.command === "string" ? server.command : void 0,
|
|
39331
|
+
args: Array.isArray(server.args) ? server.args : []
|
|
39332
|
+
};
|
|
39333
|
+
}
|
|
39334
|
+
/**
|
|
39335
|
+
* Rewrite a server's command and the argument positions that name a file it
|
|
39336
|
+
* runs (see `fileArgumentIndices`), leaving every other argument as written.
|
|
39337
|
+
* Generate and import share this, so a position generate never anchors is
|
|
39338
|
+
* never relativized on import either, and a hand-written
|
|
39339
|
+
* `${CLAUDE_PLUGIN_ROOT}/…` data argument keeps its meaning.
|
|
39340
|
+
*/
|
|
39341
|
+
function rewriteServer({ server, rewriteCommand, isPath, rewritePath }) {
|
|
39342
|
+
const { command, args } = splitCommand(server);
|
|
39343
|
+
if (command === void 0) return server;
|
|
39344
|
+
const indices = fileArgumentIndices({
|
|
39345
|
+
command,
|
|
39346
|
+
args,
|
|
39347
|
+
isPath
|
|
39348
|
+
});
|
|
39349
|
+
const rewriteArgs = (values, offset) => values.map((value, index) => indices.has(index + offset) && typeof value === "string" ? rewritePath(value) : value);
|
|
39350
|
+
const next = { ...server };
|
|
39351
|
+
if (Array.isArray(server.command)) {
|
|
39352
|
+
const inlineArgs = server.command.slice(1);
|
|
39353
|
+
next.command = [rewriteCommand(command), ...rewriteArgs(inlineArgs, 0)];
|
|
39354
|
+
if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, inlineArgs.length);
|
|
39355
|
+
} else {
|
|
39356
|
+
next.command = rewriteCommand(command);
|
|
39357
|
+
if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, 0);
|
|
39358
|
+
}
|
|
39359
|
+
return next;
|
|
39360
|
+
}
|
|
39361
|
+
/** Anchor a server's plugin-relative command and the files its runner executes. */
|
|
39362
|
+
function anchorServer(server) {
|
|
39363
|
+
return rewriteServer({
|
|
39364
|
+
server,
|
|
39365
|
+
rewriteCommand: (command) => isPluginRelativeCommand(command) ? anchorToPluginRoot(command) : command,
|
|
39366
|
+
isPath: isDotRelative,
|
|
39367
|
+
rewritePath: anchorToPluginRoot
|
|
39368
|
+
});
|
|
39369
|
+
}
|
|
39370
|
+
/** Turn the plugin-root forms generate writes back into `./`. */
|
|
39371
|
+
function relativizeServer(server) {
|
|
39372
|
+
return rewriteServer({
|
|
39373
|
+
server,
|
|
39374
|
+
rewriteCommand: relativizeFromPluginRoot,
|
|
39375
|
+
isPath: (value) => PLUGIN_ROOT_PREFIX.test(value),
|
|
39376
|
+
rewritePath: relativizeFromPluginRoot
|
|
39377
|
+
});
|
|
39378
|
+
}
|
|
39379
|
+
/** Apply a per-server transform to every server entry that is an object. */
|
|
39380
|
+
function mapServers({ mcpServers, transform }) {
|
|
39381
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return mcpServers;
|
|
39382
|
+
return Object.fromEntries(Object.entries(mcpServers).map(([name, server]) => server === null || typeof server !== "object" || Array.isArray(server) ? [name, server] : [name, transform(server)]));
|
|
39383
|
+
}
|
|
39384
|
+
/**
|
|
39385
|
+
* MCP servers bundled in a Claude Code plugin (`.mcp.json` at the plugin
|
|
39386
|
+
* root).
|
|
39387
|
+
*
|
|
39388
|
+
* Claude Code loads a plugin's `.mcp.json` with the consumer's project as the
|
|
39389
|
+
* working directory, so a relative `command` such as `./servers/db-server`
|
|
39390
|
+
* would be looked up in the consumer's repository, where the bundled binary
|
|
39391
|
+
* does not exist. Generate therefore rewrites a relative `command`, and the
|
|
39392
|
+
* `./` script a script runner executes (`node ./dist/server.js`,
|
|
39393
|
+
* `uv run ./server.py`), to `${CLAUDE_PLUGIN_ROOT}/…`, which Claude Code
|
|
39394
|
+
* substitutes with the installed plugin's directory. Other `./` arguments are
|
|
39395
|
+
* data the server reads relative to the consumer's project
|
|
39396
|
+
* (`server-filesystem ./docs`), so they are left as written, the same rule
|
|
39397
|
+
* plugin hook commands follow. Bare executables (`npx`, `node`), absolute paths
|
|
39398
|
+
* and values that already start with a variable pass through. `env` values and
|
|
39399
|
+
* the remote transports' `url` / `headers` are left as written, because
|
|
39400
|
+
* rulesync cannot tell a path from an opaque string there. Import turns the
|
|
39401
|
+
* plugin-root forms in `command` and `args` back into `./`, including the
|
|
39402
|
+
* shell-variable spellings a hand-written file may have copied from a hook
|
|
39403
|
+
* command (which Claude Code does not substitute in MCP config).
|
|
39404
|
+
*
|
|
39405
|
+
* @see https://code.claude.com/docs/en/plugins-reference
|
|
39406
|
+
*/
|
|
39407
|
+
var ClaudecodePluginMcp = class ClaudecodePluginMcp extends ClaudecodeMcp {
|
|
39408
|
+
static async fromRulesyncMcp(params) {
|
|
39409
|
+
const base = await super.fromRulesyncMcp(params);
|
|
39410
|
+
const json = base.getJson();
|
|
39411
|
+
const mcpServers = mapServers({
|
|
39412
|
+
mcpServers: json.mcpServers,
|
|
39413
|
+
transform: anchorServer
|
|
39414
|
+
});
|
|
39415
|
+
return new ClaudecodePluginMcp({
|
|
39416
|
+
outputRoot: params.outputRoot ?? process.cwd(),
|
|
39417
|
+
relativeDirPath: base.getRelativeDirPath(),
|
|
39418
|
+
relativeFilePath: base.getRelativeFilePath(),
|
|
39419
|
+
fileContent: JSON.stringify({
|
|
39420
|
+
...json,
|
|
39421
|
+
mcpServers
|
|
39422
|
+
}, null, 2),
|
|
39423
|
+
validate: params.validate ?? true,
|
|
39424
|
+
global: params.global ?? false
|
|
39425
|
+
});
|
|
39426
|
+
}
|
|
39427
|
+
static async fromFile(params) {
|
|
39428
|
+
const base = await super.fromFile(params);
|
|
39429
|
+
return new ClaudecodePluginMcp({
|
|
39430
|
+
outputRoot: params.outputRoot ?? process.cwd(),
|
|
39431
|
+
relativeDirPath: base.getRelativeDirPath(),
|
|
39432
|
+
relativeFilePath: base.getRelativeFilePath(),
|
|
39433
|
+
fileContent: base.getFileContent(),
|
|
39434
|
+
validate: params.validate ?? true,
|
|
39435
|
+
global: params.global ?? false
|
|
39436
|
+
});
|
|
39437
|
+
}
|
|
39438
|
+
toRulesyncMcp() {
|
|
39439
|
+
const mcpServers = mapServers({
|
|
39440
|
+
mcpServers: this.getJson().mcpServers ?? {},
|
|
39441
|
+
transform: relativizeServer
|
|
39442
|
+
});
|
|
39443
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
39444
|
+
}
|
|
39445
|
+
static forDeletion(params) {
|
|
39446
|
+
return new ClaudecodePluginMcp({
|
|
39447
|
+
outputRoot: params.outputRoot ?? process.cwd(),
|
|
39448
|
+
relativeDirPath: params.relativeDirPath,
|
|
39449
|
+
relativeFilePath: params.relativeFilePath,
|
|
39450
|
+
fileContent: "{}",
|
|
39451
|
+
validate: false,
|
|
39452
|
+
global: params.global ?? false
|
|
39453
|
+
});
|
|
39454
|
+
}
|
|
39455
|
+
};
|
|
39456
|
+
//#endregion
|
|
38984
39457
|
//#region src/features/mcp/cline-mcp.ts
|
|
38985
39458
|
const CLINE_GLOBAL_ONLY_MESSAGE = "Cline MCP is global-only; use --global to sync ~/.cline/data/settings/cline_mcp_settings.json";
|
|
38986
39459
|
function parseClineSettings(fileContent, relativeDirPath, relativeFilePath) {
|
|
@@ -40169,6 +40642,35 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
40169
40642
|
}
|
|
40170
40643
|
return result;
|
|
40171
40644
|
}
|
|
40645
|
+
/**
|
|
40646
|
+
* Keys Codex refuses on each transport. Its `McpServerConfig` deserializer
|
|
40647
|
+
* calls `throw_if_set` for these, and the error fails the whole `config.toml`,
|
|
40648
|
+
* not just the one server, so they are dropped with a warning instead of
|
|
40649
|
+
* written. Checked against `codex-rs/config/src/mcp_types.rs` at rust-v0.162.1.
|
|
40650
|
+
*/
|
|
40651
|
+
const CODEX_STDIO_REJECTED_KEYS = [
|
|
40652
|
+
"url",
|
|
40653
|
+
"bearer_token_env_var",
|
|
40654
|
+
"bearer_token",
|
|
40655
|
+
"http_headers_helper",
|
|
40656
|
+
"http_headers",
|
|
40657
|
+
"env_http_headers",
|
|
40658
|
+
"oauth",
|
|
40659
|
+
"oauth_resource",
|
|
40660
|
+
"auth"
|
|
40661
|
+
];
|
|
40662
|
+
const CODEX_HTTP_REJECTED_KEYS = [
|
|
40663
|
+
"args",
|
|
40664
|
+
"env",
|
|
40665
|
+
"env_vars",
|
|
40666
|
+
"cwd",
|
|
40667
|
+
"bearer_token"
|
|
40668
|
+
];
|
|
40669
|
+
function dropTransportRejectedKeys({ converted, isStdio, serverName }) {
|
|
40670
|
+
const dropped = (isStdio ? CODEX_STDIO_REJECTED_KEYS : CODEX_HTTP_REJECTED_KEYS).filter((key) => converted[key] !== void 0);
|
|
40671
|
+
for (const key of dropped) delete converted[key];
|
|
40672
|
+
if (dropped.length > 0) warnWithFallback(void 0, `[CodexCliMcp] Dropping ${dropped.map((key) => `'${key}'`).join(", ")} from ${isStdio ? "stdio" : "url-based"} MCP server "${serverName}": Codex rejects ${dropped.length === 1 ? "this key" : "these keys"} on a ${isStdio ? "stdio" : "url-based"} server and would fail to load the whole config.toml.`);
|
|
40673
|
+
}
|
|
40172
40674
|
function convertToCodexFormat(mcpServers) {
|
|
40173
40675
|
const result = {};
|
|
40174
40676
|
const originalNames = /* @__PURE__ */ new Map();
|
|
@@ -40211,6 +40713,11 @@ function convertToCodexFormat(mcpServers) {
|
|
|
40211
40713
|
converted,
|
|
40212
40714
|
serverName: name
|
|
40213
40715
|
});
|
|
40716
|
+
dropTransportRejectedKeys({
|
|
40717
|
+
converted,
|
|
40718
|
+
isStdio,
|
|
40719
|
+
serverName: name
|
|
40720
|
+
});
|
|
40214
40721
|
const previousName = originalNames.get(codexName);
|
|
40215
40722
|
if (previousName !== void 0) warnWithFallback(void 0, `Codex MCP server name collision: "${previousName}" and "${name}" both normalize to "${codexName}"; "${name}" (processed last) overwrites "${previousName}".`);
|
|
40216
40723
|
originalNames.set(codexName, name);
|
|
@@ -41690,7 +42197,7 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
41690
42197
|
fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
|
|
41691
42198
|
feature: "mcp",
|
|
41692
42199
|
existingContent,
|
|
41693
|
-
patch: { ["mcp"]: converted },
|
|
42200
|
+
patch: { ["mcp"]: undefinedWhenEmpty(converted) },
|
|
41694
42201
|
filePath: location.filePath,
|
|
41695
42202
|
logger
|
|
41696
42203
|
}),
|
|
@@ -43270,13 +43777,23 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
43270
43777
|
//#region src/features/mcp/grokcli-mcp.ts
|
|
43271
43778
|
const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
|
|
43272
43779
|
/**
|
|
43780
|
+
* Grok keeps per-server MCP tool deny lists in a top-level
|
|
43781
|
+
* `[disabled_mcp_tools]` table (`map<server, string[]>` of unqualified tool
|
|
43782
|
+
* names), not inside `[mcp_servers.<name>]`. Grok reads it from the user
|
|
43783
|
+
* `~/.grok/config.toml` only: a project `.grok/config.toml` contributes just
|
|
43784
|
+
* `[mcp_servers]`, `[plugins]`, `[permission]` and `[mcp] max_output_bytes`.
|
|
43785
|
+
* https://github.com/xai-org/grok-build/blob/2bdd1d6a/crates/codegen/xai-grok-pager/docs/user-guide/26-config-reference.md
|
|
43786
|
+
*/
|
|
43787
|
+
const GROK_DISABLED_MCP_TOOLS_KEY = "disabled_mcp_tools";
|
|
43788
|
+
/**
|
|
43273
43789
|
* Grok Build stores MCP servers in `config.toml` under a `[mcp_servers.<name>]`
|
|
43274
43790
|
* table. Verified against `grok mcp add` (grok 0.2.54): a stdio server emits
|
|
43275
43791
|
* `command`, `args`, `enabled = true`, and an `[mcp_servers.<name>.env]` table;
|
|
43276
43792
|
* a remote server emits `url` and `enabled`. Unlike Codex CLI, Grok uses a
|
|
43277
|
-
* literal `env` table (not the `env_vars` passthrough list)
|
|
43278
|
-
*
|
|
43279
|
-
*
|
|
43793
|
+
* literal `env` table (not the `env_vars` passthrough list), so the only field
|
|
43794
|
+
* rename is `disabled` (rulesync) ↔ `enabled = false` (grok). `disabledTools`
|
|
43795
|
+
* never lands in the server table: it is lifted into the top-level
|
|
43796
|
+
* `[disabled_mcp_tools]` table by `buildDisabledMcpTools`.
|
|
43280
43797
|
*/
|
|
43281
43798
|
function convertToGrokFormat(mcpServers) {
|
|
43282
43799
|
const result = {};
|
|
@@ -43285,7 +43802,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
43285
43802
|
if (!isRecord$1(config)) continue;
|
|
43286
43803
|
const converted = {};
|
|
43287
43804
|
for (const [key, value] of Object.entries(config)) {
|
|
43288
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
43805
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key) || key === "disabledTools") continue;
|
|
43289
43806
|
if (key === "disabled") {
|
|
43290
43807
|
if (value === true) converted["enabled"] = false;
|
|
43291
43808
|
} else converted[key] = value;
|
|
@@ -43309,6 +43826,38 @@ function convertFromGrokFormat(grokMcp) {
|
|
|
43309
43826
|
}
|
|
43310
43827
|
return result;
|
|
43311
43828
|
}
|
|
43829
|
+
function toToolNameList(value) {
|
|
43830
|
+
if (!Array.isArray(value)) return [];
|
|
43831
|
+
return value.filter((tool) => typeof tool === "string");
|
|
43832
|
+
}
|
|
43833
|
+
/**
|
|
43834
|
+
* Recompute the global `[disabled_mcp_tools]` table: every server Rulesync
|
|
43835
|
+
* declares gets its `disabledTools` (or loses a stale entry when it has none —
|
|
43836
|
+
* Grok itself removes an entry whose list empties), while entries for other
|
|
43837
|
+
* servers — ones toggled in Grok's `/mcps` UI, plugin or compat servers, and
|
|
43838
|
+
* the `__managed_gateway_connectors` sentinel — are carried over untouched.
|
|
43839
|
+
* Returns `undefined` when nothing is left, so the key is retracted.
|
|
43840
|
+
*/
|
|
43841
|
+
function buildDisabledMcpTools({ mcpServers, existingContent, filePath }) {
|
|
43842
|
+
const existing = parseSharedConfig({
|
|
43843
|
+
format: "toml",
|
|
43844
|
+
fileContent: existingContent,
|
|
43845
|
+
filePath
|
|
43846
|
+
})[GROK_DISABLED_MCP_TOOLS_KEY];
|
|
43847
|
+
const result = isRecord$1(existing) ? { ...existing } : {};
|
|
43848
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
43849
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
43850
|
+
const tools = toToolNameList(config.disabledTools);
|
|
43851
|
+
if (tools.length > 0) result[name] = tools;
|
|
43852
|
+
else delete result[name];
|
|
43853
|
+
}
|
|
43854
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
43855
|
+
}
|
|
43856
|
+
function warnProjectDisabledTools(mcpServers, logger) {
|
|
43857
|
+
const serverNames = Object.entries(mcpServers).filter(([, config]) => isRecord$1(config) && config.disabledTools !== void 0).map(([name]) => name);
|
|
43858
|
+
if (serverNames.length === 0) return;
|
|
43859
|
+
warnWithFallback(logger, `grokcli reads per-server \`disabledTools\` only from the user ~/.grok/config.toml ([disabled_mcp_tools]); dropping it from ${serverNames.join(", ")} in project mode. Generate with --global to apply it.`);
|
|
43860
|
+
}
|
|
43312
43861
|
var GrokcliMcp = class GrokcliMcp extends ToolMcp {
|
|
43313
43862
|
toml;
|
|
43314
43863
|
constructor({ ...rest }) {
|
|
@@ -43345,16 +43894,19 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
|
|
|
43345
43894
|
relativeDirPath: paths.relativeDirPath,
|
|
43346
43895
|
relativeFilePath: paths.relativeFilePath,
|
|
43347
43896
|
fileContent,
|
|
43348
|
-
validate
|
|
43897
|
+
validate,
|
|
43898
|
+
global
|
|
43349
43899
|
});
|
|
43350
43900
|
}
|
|
43351
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
43901
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
43352
43902
|
const paths = this.getSettablePaths({ global });
|
|
43353
43903
|
const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43354
43904
|
const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
|
|
43355
|
-
const
|
|
43905
|
+
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
43906
|
+
const converted = convertToGrokFormat(strippedMcpServers);
|
|
43356
43907
|
const filteredMcpServers = this.removeEmptyEntries(converted);
|
|
43357
|
-
|
|
43908
|
+
if (!global) warnProjectDisabledTools(strippedMcpServers, logger);
|
|
43909
|
+
for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(logger, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
|
|
43358
43910
|
return new GrokcliMcp({
|
|
43359
43911
|
outputRoot,
|
|
43360
43912
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -43363,14 +43915,31 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
|
|
|
43363
43915
|
fileKey: sharedConfigFileKey(paths),
|
|
43364
43916
|
feature: "mcp",
|
|
43365
43917
|
existingContent: configTomlFileContent,
|
|
43366
|
-
patch: {
|
|
43918
|
+
patch: {
|
|
43919
|
+
mcp_servers: Object.keys(filteredMcpServers).length > 0 ? filteredMcpServers : void 0,
|
|
43920
|
+
...global && { [GROK_DISABLED_MCP_TOOLS_KEY]: buildDisabledMcpTools({
|
|
43921
|
+
mcpServers: strippedMcpServers,
|
|
43922
|
+
existingContent: configTomlFileContent,
|
|
43923
|
+
filePath: configTomlFilePath
|
|
43924
|
+
}) }
|
|
43925
|
+
},
|
|
43367
43926
|
filePath: configTomlFilePath
|
|
43368
43927
|
}),
|
|
43369
|
-
validate
|
|
43928
|
+
validate,
|
|
43929
|
+
global
|
|
43370
43930
|
});
|
|
43371
43931
|
}
|
|
43372
43932
|
toRulesyncMcp() {
|
|
43373
43933
|
const converted = convertFromGrokFormat(this.toml.mcp_servers ?? {});
|
|
43934
|
+
const disabledMcpTools = this.toml[GROK_DISABLED_MCP_TOOLS_KEY];
|
|
43935
|
+
if (this.global && isRecord$1(disabledMcpTools)) for (const [name, config] of Object.entries(converted)) {
|
|
43936
|
+
if (!Object.hasOwn(disabledMcpTools, name)) continue;
|
|
43937
|
+
const tools = toToolNameList(disabledMcpTools[name]);
|
|
43938
|
+
if (tools.length > 0) converted[name] = {
|
|
43939
|
+
...config,
|
|
43940
|
+
disabledTools: tools
|
|
43941
|
+
};
|
|
43942
|
+
}
|
|
43374
43943
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
43375
43944
|
}
|
|
43376
43945
|
validate() {
|
|
@@ -46434,7 +47003,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
46434
47003
|
fileKey: sharedConfigFileKey(paths),
|
|
46435
47004
|
feature: "mcp",
|
|
46436
47005
|
existingContent,
|
|
46437
|
-
patch: { [POOL_MCP_SERVERS_KEY]: converted },
|
|
47006
|
+
patch: { [POOL_MCP_SERVERS_KEY]: undefinedWhenEmpty(converted) },
|
|
46438
47007
|
filePath
|
|
46439
47008
|
}),
|
|
46440
47009
|
validate,
|
|
@@ -46564,7 +47133,7 @@ var QoderMcp = class QoderMcp extends ToolMcp {
|
|
|
46564
47133
|
fileKey: sharedConfigFileKey(paths),
|
|
46565
47134
|
feature: "mcp",
|
|
46566
47135
|
existingContent,
|
|
46567
|
-
patch: { mcpServers },
|
|
47136
|
+
patch: { mcpServers: undefinedWhenEmpty(mcpServers) },
|
|
46568
47137
|
filePath
|
|
46569
47138
|
}) : JSON.stringify({
|
|
46570
47139
|
...parseQoderMcpConfig({
|
|
@@ -46943,18 +47512,28 @@ function warnAboutRetiredFields({ name, serverRecord, logger }) {
|
|
|
46943
47512
|
* `" Serial "` counts too and is written as authored.
|
|
46944
47513
|
*/
|
|
46945
47514
|
const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
|
|
46946
|
-
/**
|
|
46947
|
-
* The type Reasonix decodes each fixed-type scalar passthrough field into
|
|
46948
|
-
* (`PluginEntry` in `internal/config/plugin_entry.go`, and
|
|
46949
|
-
* `internal/contract/config/plugin_entry.go` on the CLI v2 line).
|
|
46950
|
-
*/
|
|
46951
47515
|
const REASONIX_TYPED_PLUGIN_FIELDS = {
|
|
47516
|
+
startup_timeout_seconds: "integer",
|
|
47517
|
+
call_timeout_seconds: "integer",
|
|
47518
|
+
tool_timeout_seconds: "integer table",
|
|
46952
47519
|
concurrency: "string",
|
|
46953
47520
|
auto_start: "boolean",
|
|
46954
47521
|
load: "string",
|
|
46955
47522
|
oauth_allow_missing_pkce_metadata: "boolean"
|
|
46956
47523
|
};
|
|
46957
47524
|
/**
|
|
47525
|
+
* Whether `value` is written as a TOML integer. smol-toml writes a JS number
|
|
47526
|
+
* outside the safe-integer range, or with a fraction, as a TOML float.
|
|
47527
|
+
*/
|
|
47528
|
+
function isTomlInteger(value) {
|
|
47529
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
47530
|
+
}
|
|
47531
|
+
function hasExpectedType(value, expected) {
|
|
47532
|
+
if (expected === "integer") return isTomlInteger(value);
|
|
47533
|
+
if (expected === "integer table") return isRecord$1(value) && Object.values(value).every(isTomlInteger);
|
|
47534
|
+
return typeof value === expected;
|
|
47535
|
+
}
|
|
47536
|
+
/**
|
|
46958
47537
|
* Why a passthrough value cannot be written, or `undefined` when it can (and
|
|
46959
47538
|
* for every field without a fixed type). A wrong type is the case that matters:
|
|
46960
47539
|
* a TOML type mismatch makes Reasonix refuse the whole config file. An unknown
|
|
@@ -46966,7 +47545,7 @@ const REASONIX_TYPED_PLUGIN_FIELDS = {
|
|
|
46966
47545
|
*/
|
|
46967
47546
|
function invalidPassthroughFieldReason(field, value) {
|
|
46968
47547
|
const expected = REASONIX_TYPED_PLUGIN_FIELDS[field];
|
|
46969
|
-
if (expected !== void 0 &&
|
|
47548
|
+
if (expected !== void 0 && !hasExpectedType(value, expected)) return `Reasonix expects ${expected === "integer" ? "an" : "a"} ${expected === "integer table" ? "table of integers" : expected} and fails to load a config file holding any other type.`;
|
|
46970
47549
|
if (field === "concurrency" && typeof value === "string" && !REASONIX_CONCURRENCY_VALUES.has(value.trim().toLowerCase())) return `Reasonix accepts only "serial" or "parallel" and ignores ${JSON.stringify(value)}, falling back to the server-name default.`;
|
|
46971
47550
|
}
|
|
46972
47551
|
/**
|
|
@@ -47988,7 +48567,7 @@ var TabnineMcp = class TabnineMcp extends ToolMcp {
|
|
|
47988
48567
|
fileKey: sharedConfigFileKey(paths),
|
|
47989
48568
|
feature: "mcp",
|
|
47990
48569
|
existingContent,
|
|
47991
|
-
patch: { mcpServers: convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger) },
|
|
48570
|
+
patch: { mcpServers: undefinedWhenEmpty(convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger)) },
|
|
47992
48571
|
filePath
|
|
47993
48572
|
}),
|
|
47994
48573
|
validate,
|
|
@@ -49309,6 +49888,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
|
|
|
49309
49888
|
const paths = this.getSettablePaths({ global });
|
|
49310
49889
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
49311
49890
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
49891
|
+
const contextServers = convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger);
|
|
49312
49892
|
return new ZedMcp({
|
|
49313
49893
|
outputRoot,
|
|
49314
49894
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -49317,7 +49897,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
|
|
|
49317
49897
|
fileKey: sharedConfigFileKey(paths),
|
|
49318
49898
|
feature: "mcp",
|
|
49319
49899
|
existingContent,
|
|
49320
|
-
patch: { context_servers:
|
|
49900
|
+
patch: { context_servers: Object.keys(contextServers).length > 0 ? contextServers : void 0 },
|
|
49321
49901
|
filePath
|
|
49322
49902
|
}),
|
|
49323
49903
|
validate
|
|
@@ -49450,7 +50030,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
49450
50030
|
}
|
|
49451
50031
|
}],
|
|
49452
50032
|
["claudecode-plugin", {
|
|
49453
|
-
class:
|
|
50033
|
+
class: ClaudecodePluginMcp,
|
|
49454
50034
|
meta: {
|
|
49455
50035
|
supportsProject: true,
|
|
49456
50036
|
supportsGlobal: false,
|
|
@@ -49626,7 +50206,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
49626
50206
|
supportsProject: true,
|
|
49627
50207
|
supportsGlobal: true,
|
|
49628
50208
|
supportsEnabledTools: false,
|
|
49629
|
-
supportsDisabledTools:
|
|
50209
|
+
supportsDisabledTools: true
|
|
49630
50210
|
}
|
|
49631
50211
|
}],
|
|
49632
50212
|
["hermesagent", {
|
|
@@ -50792,6 +51372,8 @@ function parsedGlobsIntersect(left, right, budget) {
|
|
|
50792
51372
|
* @see https://antigravity.google/docs/permissions?tab=cli
|
|
50793
51373
|
*/
|
|
50794
51374
|
const REGEX_PREFIX = "regex:";
|
|
51375
|
+
const TRAILING_ANCHOR = /(?<!\\)\$$/;
|
|
51376
|
+
const PLAIN_TARGET_GLOB_CHARACTERS = /[*?[{\\]/;
|
|
50795
51377
|
const REGEX_METACHARACTERS$1 = /[\\^$.|?*+()[\]{}]/;
|
|
50796
51378
|
const WHITESPACE_CODES = [
|
|
50797
51379
|
9,
|
|
@@ -50932,6 +51514,7 @@ function replacePosixClasses(source) {
|
|
|
50932
51514
|
function isInvalidRegex(source) {
|
|
50933
51515
|
const unquoted = source.replace(RE2_QUOTED, (match) => match === "\\\\" ? match : "q");
|
|
50934
51516
|
if (LOOKAROUND.test(unquoted) || hasRefusedEscape(unquoted)) return true;
|
|
51517
|
+
if (hasClassEscapeRangeEnd(unquoted)) return true;
|
|
50935
51518
|
const withoutPosixClasses = replacePosixClasses(unquoted);
|
|
50936
51519
|
if (withoutPosixClasses === void 0 || hasRefusedRepeat(withoutPosixClasses)) return true;
|
|
50937
51520
|
const asJavaScript = withoutPosixClasses.replace(/\(\?P</g, "(?<").replace(/\(\?[imsU]*-?[imsU]*\)/g, "").replace(/\(\?[imsU]*-?[imsU]*:/g, "(?:").replace(/\\A/g, "^").replace(/\\z/g, "$");
|
|
@@ -50956,6 +51539,74 @@ function hasRefusedEscape(source) {
|
|
|
50956
51539
|
}
|
|
50957
51540
|
return false;
|
|
50958
51541
|
}
|
|
51542
|
+
const CLASS_ESCAPE_LETTERS = /[dDsSwWpP]/;
|
|
51543
|
+
/** The length of the escape at `index`, braces (`\x{41}`, `\p{Greek}`) included. */
|
|
51544
|
+
function escapeLength(source, index) {
|
|
51545
|
+
const escaped = source[index + 1] ?? "";
|
|
51546
|
+
if (/[xpP]/.test(escaped) && source[index + 2] === "{") {
|
|
51547
|
+
const close = source.indexOf("}", index + 3);
|
|
51548
|
+
return close === -1 ? source.length - index : close - index + 1;
|
|
51549
|
+
}
|
|
51550
|
+
if (escaped === "p" || escaped === "P") return 3;
|
|
51551
|
+
return escaped === "x" ? 4 : 2;
|
|
51552
|
+
}
|
|
51553
|
+
/** Step past `^` and a leading `]` (a member) after the `[` that ends at `index`. */
|
|
51554
|
+
function openClass(source, start) {
|
|
51555
|
+
const index = source[start] === "^" ? start + 1 : start;
|
|
51556
|
+
return source[index] === "]" ? {
|
|
51557
|
+
index: index + 1,
|
|
51558
|
+
afterCharacter: true
|
|
51559
|
+
} : {
|
|
51560
|
+
index,
|
|
51561
|
+
afterCharacter: false
|
|
51562
|
+
};
|
|
51563
|
+
}
|
|
51564
|
+
/**
|
|
51565
|
+
* Whether a bracket range ends in a class escape (`[a-\d]`, `[--\s]`). Go
|
|
51566
|
+
* reads the end of a range as one character and refuses such a regex, while
|
|
51567
|
+
* JavaScript without the `u` flag reads it as three members. A class escape
|
|
51568
|
+
* that starts the range (`[\d-z]`) is a member followed by `-`, which both
|
|
51569
|
+
* accept. The `u` flag cannot be used instead: it also refuses RE2 spellings
|
|
51570
|
+
* such as `\x{41}` and `\-` outside a class.
|
|
51571
|
+
*/
|
|
51572
|
+
function hasClassEscapeRangeEnd(source) {
|
|
51573
|
+
let inClass = false;
|
|
51574
|
+
let afterCharacter = false;
|
|
51575
|
+
let index = 0;
|
|
51576
|
+
while (index < source.length) {
|
|
51577
|
+
const character = source[index] ?? "";
|
|
51578
|
+
if (character === "\\") {
|
|
51579
|
+
afterCharacter = inClass && !CLASS_ESCAPE_LETTERS.test(source[index + 1] ?? "");
|
|
51580
|
+
index += escapeLength(source, index);
|
|
51581
|
+
continue;
|
|
51582
|
+
}
|
|
51583
|
+
if (!inClass) {
|
|
51584
|
+
inClass = character === "[";
|
|
51585
|
+
({index, afterCharacter} = inClass ? openClass(source, index + 1) : {
|
|
51586
|
+
index: index + 1,
|
|
51587
|
+
afterCharacter: false
|
|
51588
|
+
});
|
|
51589
|
+
continue;
|
|
51590
|
+
}
|
|
51591
|
+
const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
|
|
51592
|
+
if (character === "]" || posixEnd !== -1) {
|
|
51593
|
+
inClass = character !== "]";
|
|
51594
|
+
afterCharacter = false;
|
|
51595
|
+
index = posixEnd === -1 ? index + 1 : posixEnd + 2;
|
|
51596
|
+
continue;
|
|
51597
|
+
}
|
|
51598
|
+
if (character === "-" && afterCharacter && source[index + 1] !== "]") {
|
|
51599
|
+
const end = index + 1;
|
|
51600
|
+
if (source[end] === "\\" && CLASS_ESCAPE_LETTERS.test(source[end + 1] ?? "")) return true;
|
|
51601
|
+
afterCharacter = false;
|
|
51602
|
+
index = end + (source[end] === "\\" ? escapeLength(source, end) : 1);
|
|
51603
|
+
continue;
|
|
51604
|
+
}
|
|
51605
|
+
afterCharacter = true;
|
|
51606
|
+
index += 1;
|
|
51607
|
+
}
|
|
51608
|
+
return false;
|
|
51609
|
+
}
|
|
50959
51610
|
/**
|
|
50960
51611
|
* Whether a repeat count is over Go's limit. Braces inside a bracket class
|
|
50961
51612
|
* (`[a{1001}]`) and in `\x{2003}` or `\p{Greek}` are not repeats, so those
|
|
@@ -51103,7 +51754,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
|
|
|
51103
51754
|
* the rule a regex.
|
|
51104
51755
|
*/
|
|
51105
51756
|
function regexWordToGlob(word) {
|
|
51106
|
-
const body = word.replace(/^\^/, "").replace(
|
|
51757
|
+
const body = word.replace(/^\^/, "").replace(TRAILING_ANCHOR, "");
|
|
51107
51758
|
const characters = Array.from(body);
|
|
51108
51759
|
if (characters.length === 0) return;
|
|
51109
51760
|
let glob = "";
|
|
@@ -51144,12 +51795,61 @@ function regexToGlob(source) {
|
|
|
51144
51795
|
function fromAntigravityCommandTarget(target) {
|
|
51145
51796
|
if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
|
|
51146
51797
|
if (target === "*") return target;
|
|
51147
|
-
if (
|
|
51798
|
+
if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
|
|
51148
51799
|
const words = splitWords(target);
|
|
51149
51800
|
return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
|
|
51150
51801
|
}
|
|
51151
51802
|
return `${target} *`;
|
|
51152
51803
|
}
|
|
51804
|
+
/**
|
|
51805
|
+
* Whether a regex word means the same anchored or not: each end is either
|
|
51806
|
+
* anchored (`^`, an unescaped `$`) or open-ended (`.*`).
|
|
51807
|
+
*/
|
|
51808
|
+
function isAnchoredWord(word) {
|
|
51809
|
+
const startAnchored = word.startsWith("^") || word.startsWith(".*");
|
|
51810
|
+
const endAnchored = TRAILING_ANCHOR.test(word) || word.endsWith(".*");
|
|
51811
|
+
return startAnchored && endAnchored;
|
|
51812
|
+
}
|
|
51813
|
+
/**
|
|
51814
|
+
* Explain what importing an Antigravity `command` target may lose, or return
|
|
51815
|
+
* `undefined` when the import is faithful.
|
|
51816
|
+
*
|
|
51817
|
+
* - A `regex:` target with a word that is not anchored is imported as a glob
|
|
51818
|
+
* that assumes anchoring. If Antigravity matches such a word unanchored,
|
|
51819
|
+
* the imported rule is narrower (`regex:rm` also matches `rmdir`).
|
|
51820
|
+
* - A plain target holding characters that are special in a glob is most
|
|
51821
|
+
* likely the verbatim output of an older rulesync (`command(rm -rf *)`).
|
|
51822
|
+
* Antigravity matches it literally, so it imports as a regex that matches a
|
|
51823
|
+
* literal `*` word and is inert in the other tools it is exported to.
|
|
51824
|
+
*/
|
|
51825
|
+
function describeAntigravityCommandImportLoss(target) {
|
|
51826
|
+
if (target.startsWith(REGEX_PREFIX)) {
|
|
51827
|
+
const source = target.slice(6);
|
|
51828
|
+
if (splitWords(source).some((word) => !isAnchoredWord(word)) && regexToGlob(source) !== void 0) return {
|
|
51829
|
+
reason: "it has a regex word without `^...$` anchors and is imported as a glob that assumes Antigravity anchors every word, so the imported rule may be narrower than the original",
|
|
51830
|
+
denyConsequence: "The imported deny may block less than it does in Antigravity."
|
|
51831
|
+
};
|
|
51832
|
+
return;
|
|
51833
|
+
}
|
|
51834
|
+
if (target !== "*" && PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
|
|
51835
|
+
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",
|
|
51836
|
+
denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
|
|
51837
|
+
};
|
|
51838
|
+
}
|
|
51839
|
+
/**
|
|
51840
|
+
* Import one Antigravity `command` target as a canonical bash pattern, warning
|
|
51841
|
+
* when the result may differ from what Antigravity enforces. Shared by the
|
|
51842
|
+
* Antigravity CLI and IDE importers.
|
|
51843
|
+
*/
|
|
51844
|
+
function importAntigravityCommandTarget({ target, entry, decision, logger, toolLabel }) {
|
|
51845
|
+
const pattern = fromAntigravityCommandTarget(target);
|
|
51846
|
+
const loss = describeAntigravityCommandImportLoss(target);
|
|
51847
|
+
if (loss !== void 0) {
|
|
51848
|
+
const consequence = decision === "deny" ? ` ${loss.denyConsequence}` : "";
|
|
51849
|
+
logger.warn(`${toolLabel} permissions: importing ${entry} as "${pattern}": ${loss.reason}.${consequence}`);
|
|
51850
|
+
}
|
|
51851
|
+
return pattern;
|
|
51852
|
+
}
|
|
51153
51853
|
//#endregion
|
|
51154
51854
|
//#region src/features/permissions/antigravity-targets.ts
|
|
51155
51855
|
/**
|
|
@@ -51180,6 +51880,7 @@ const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
|
|
|
51180
51880
|
"**/*"
|
|
51181
51881
|
]);
|
|
51182
51882
|
const DIRECTORY_CONTENTS_SUFFIX = /\/\*\*(?:\/\*)?$/;
|
|
51883
|
+
const HOME_OR_VARIABLE_PATH = /^~(?:\/|$)|^\$[A-Za-z_]|\$\{[A-Za-z_]/;
|
|
51183
51884
|
const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
|
|
51184
51885
|
const DENY_HINTS = {
|
|
51185
51886
|
read_file: "deny a directory or file path instead",
|
|
@@ -51262,6 +51963,10 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
|
|
|
51262
51963
|
return;
|
|
51263
51964
|
}
|
|
51264
51965
|
if (result.note !== void 0 && decision !== "deny") logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}.`);
|
|
51966
|
+
if (PATH_ACTIONS.has(action) && HOME_OR_VARIABLE_PATH.test(result.target)) {
|
|
51967
|
+
const consequence = decision === "deny" ? "this deny may block nothing" : `${toolLabel} may fall back to its default`;
|
|
51968
|
+
logger.warn(`${toolLabel} permissions: ${rule}: \`~\` and environment variables may not be expanded in Antigravity path targets, so ${action}(${result.target}) may match nothing and ${consequence}; use an absolute path instead.`);
|
|
51969
|
+
}
|
|
51265
51970
|
return result.target === "*" ? action : `${action}(${result.target})`;
|
|
51266
51971
|
}
|
|
51267
51972
|
//#endregion
|
|
@@ -51828,9 +52533,9 @@ function convertRulesyncToAntigravityCliPermissions({ config, logger }) {
|
|
|
51828
52533
|
}
|
|
51829
52534
|
}
|
|
51830
52535
|
return {
|
|
51831
|
-
allow,
|
|
51832
|
-
ask,
|
|
51833
|
-
deny
|
|
52536
|
+
allow: [...new Set(allow)],
|
|
52537
|
+
ask: [...new Set(ask)],
|
|
52538
|
+
deny: [...new Set(deny)]
|
|
51834
52539
|
};
|
|
51835
52540
|
}
|
|
51836
52541
|
/**
|
|
@@ -51842,7 +52547,13 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
51842
52547
|
for (const entry of entries) {
|
|
51843
52548
|
const { toolName, pattern: target } = parsePermissionEntry$1(entry);
|
|
51844
52549
|
const canonical = toCanonicalToolName$9(toolName);
|
|
51845
|
-
const pattern = toolName === "command"
|
|
52550
|
+
const pattern = toolName === "command" ? importAntigravityCommandTarget({
|
|
52551
|
+
target,
|
|
52552
|
+
entry,
|
|
52553
|
+
decision: action,
|
|
52554
|
+
logger: moduleLogger$4,
|
|
52555
|
+
toolLabel: "Antigravity CLI"
|
|
52556
|
+
}) : target;
|
|
51846
52557
|
if (!permission[canonical]) permission[canonical] = {};
|
|
51847
52558
|
permission[canonical][pattern] = action;
|
|
51848
52559
|
}
|
|
@@ -52046,9 +52757,9 @@ function convertRulesyncToAntigravityIdePermissions({ config, logger }) {
|
|
|
52046
52757
|
}
|
|
52047
52758
|
}
|
|
52048
52759
|
return {
|
|
52049
|
-
allow,
|
|
52050
|
-
ask,
|
|
52051
|
-
deny
|
|
52760
|
+
allow: [...new Set(allow)],
|
|
52761
|
+
ask: [...new Set(ask)],
|
|
52762
|
+
deny: [...new Set(deny)]
|
|
52052
52763
|
};
|
|
52053
52764
|
}
|
|
52054
52765
|
/**
|
|
@@ -52060,7 +52771,13 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
52060
52771
|
for (const entry of entries) {
|
|
52061
52772
|
const { action: ideAction, pattern: target } = parsePermissionEntry(entry);
|
|
52062
52773
|
const canonical = toCanonicalCategory$2(ideAction);
|
|
52063
|
-
const pattern = ideAction === "command"
|
|
52774
|
+
const pattern = ideAction === "command" ? importAntigravityCommandTarget({
|
|
52775
|
+
target,
|
|
52776
|
+
entry,
|
|
52777
|
+
decision: action,
|
|
52778
|
+
logger: fallbackLogger,
|
|
52779
|
+
toolLabel: "Antigravity IDE"
|
|
52780
|
+
}) : target;
|
|
52064
52781
|
if (!permission[canonical]) permission[canonical] = {};
|
|
52065
52782
|
permission[canonical][pattern] = action;
|
|
52066
52783
|
}
|
|
@@ -54042,7 +54759,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
|
|
|
54042
54759
|
managedSourcesBehavior: "managed settings",
|
|
54043
54760
|
modelPricing: "managed settings",
|
|
54044
54761
|
parentSettingsBehavior: "managed settings",
|
|
54045
|
-
permissionExplainerEnabled: "~/.claude.json",
|
|
54046
54762
|
pluginSuggestionMarketplaces: "managed settings",
|
|
54047
54763
|
pluginTrustMessage: "managed settings",
|
|
54048
54764
|
prStatusFooterEnabled: "~/.claude.json",
|
|
@@ -54051,10 +54767,24 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
|
|
|
54051
54767
|
sshHostAllowlist: "managed settings",
|
|
54052
54768
|
strictKnownMarketplaces: "managed settings",
|
|
54053
54769
|
strictPluginOnlyCustomization: "managed settings",
|
|
54054
|
-
teammateDefaultModel: "~/.claude.json",
|
|
54055
54770
|
wslInheritsWindowsSettings: "managed settings"
|
|
54056
54771
|
};
|
|
54057
54772
|
/**
|
|
54773
|
+
* Top-level settings keys Claude Code has removed, with the version that
|
|
54774
|
+
* removed each. Current versions ignore them wherever they are set, so they
|
|
54775
|
+
* are dropped in **both** scopes, and the warning names the version so the
|
|
54776
|
+
* author knows to delete the key rather than move it to another file.
|
|
54777
|
+
*
|
|
54778
|
+
* Derived from the "Removed in" notes of the settings reference.
|
|
54779
|
+
*
|
|
54780
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
54781
|
+
*/
|
|
54782
|
+
const CLAUDECODE_REMOVED_KEYS = {
|
|
54783
|
+
permissionExplainerEnabled: "v2.1.257",
|
|
54784
|
+
taskOutputMaxChars: "v2.1.277",
|
|
54785
|
+
teammateDefaultModel: "v2.1.234"
|
|
54786
|
+
};
|
|
54787
|
+
/**
|
|
54058
54788
|
* Top-level settings keys whose value Claude Code **executes**. The generic
|
|
54059
54789
|
* passthrough refuses them outright rather than warning: `.rulesync/*` files
|
|
54060
54790
|
* are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
|
|
@@ -54334,6 +55064,15 @@ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
|
|
|
54334
55064
|
allowedMarketplaces: "strictKnownMarketplaces"
|
|
54335
55065
|
};
|
|
54336
55066
|
/**
|
|
55067
|
+
* The warning for a key that neither scope can honor — one Claude Code has
|
|
55068
|
+
* removed, or one read only from a file rulesync does not generate — or
|
|
55069
|
+
* `undefined` when the key may be written in at least one scope.
|
|
55070
|
+
*/
|
|
55071
|
+
function neverHonoredKeyWarning({ canonicalKey, shown, relativeFilePath }) {
|
|
55072
|
+
if (Object.hasOwn(CLAUDECODE_REMOVED_KEYS, canonicalKey)) return `Claude Code permissions: '${shown}' was removed in Claude Code ${CLAUDECODE_REMOVED_KEYS[canonicalKey]} and has no effect on current versions, so it is not written to ${relativeFilePath}. Delete it from .rulesync/permissions.jsonc, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`;
|
|
55073
|
+
if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) return `Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`;
|
|
55074
|
+
}
|
|
55075
|
+
/**
|
|
54337
55076
|
* Copy of the authored top-level passthrough with the keys the target file
|
|
54338
55077
|
* cannot honor removed, warning once per dropped key. Like
|
|
54339
55078
|
* `stripSandboxPaths`, only the override copy is filtered — a value
|
|
@@ -54350,8 +55089,13 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
|
|
|
54350
55089
|
logger?.warn(`Claude Code permissions: '${shown}' ${CLAUDECODE_COMMAND_EXECUTING_KEYS[canonicalKey]}, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; author commands in .rulesync/hooks.jsonc, or set this key in ${relativeFilePath} by hand.`);
|
|
54351
55090
|
continue;
|
|
54352
55091
|
}
|
|
54353
|
-
|
|
54354
|
-
|
|
55092
|
+
const neverHonored = neverHonoredKeyWarning({
|
|
55093
|
+
canonicalKey,
|
|
55094
|
+
shown,
|
|
55095
|
+
relativeFilePath
|
|
55096
|
+
});
|
|
55097
|
+
if (neverHonored !== void 0) {
|
|
55098
|
+
logger?.warn(neverHonored);
|
|
54355
55099
|
continue;
|
|
54356
55100
|
}
|
|
54357
55101
|
if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
|
|
@@ -62042,8 +62786,10 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
|
|
|
62042
62786
|
* table. The gateway replaces the owned `permission` key wholesale and rulesync
|
|
62043
62787
|
* only ever authors `rules`, so a hand-written sibling such as
|
|
62044
62788
|
* `dangerous_command_guard = false` (Kimi Code 0.40.0) would otherwise be
|
|
62045
|
-
* deleted on every generate.
|
|
62046
|
-
*
|
|
62789
|
+
* deleted on every generate. Unmodeled keys pass through verbatim; `rules`
|
|
62790
|
+
* always comes from the patch, and so does `dangerous_command_guard` when the
|
|
62791
|
+
* `kimi-code.dangerousCommandGuard` override sets it. Leaving the override out
|
|
62792
|
+
* keeps whatever the file already has.
|
|
62047
62793
|
*
|
|
62048
62794
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
|
|
62049
62795
|
*/
|
|
@@ -62286,14 +63032,18 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
|
|
|
62286
63032
|
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, logger }) {
|
|
62287
63033
|
const config = rulesyncPermissions.getJson();
|
|
62288
63034
|
const defaultPermissionMode = config["kimi-code"]?.defaultPermissionMode;
|
|
63035
|
+
const dangerousCommandGuard = config["kimi-code"]?.dangerousCommandGuard;
|
|
62289
63036
|
const tools = buildKimiCodeToolsSection(config["kimi-code"]?.tools);
|
|
62290
63037
|
const document = {
|
|
62291
63038
|
...defaultPermissionMode && { default_permission_mode: defaultPermissionMode },
|
|
62292
63039
|
...tools && { tools },
|
|
62293
|
-
permission: {
|
|
62294
|
-
|
|
62295
|
-
|
|
62296
|
-
|
|
63040
|
+
permission: {
|
|
63041
|
+
...typeof dangerousCommandGuard === "boolean" && { dangerous_command_guard: dangerousCommandGuard },
|
|
63042
|
+
rules: canonicalToKimiCodeRules({
|
|
63043
|
+
config,
|
|
63044
|
+
logger
|
|
63045
|
+
})
|
|
63046
|
+
}
|
|
62297
63047
|
};
|
|
62298
63048
|
let fileContent;
|
|
62299
63049
|
try {
|
|
@@ -62315,11 +63065,14 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
|
|
|
62315
63065
|
format: "toml",
|
|
62316
63066
|
fileContent: this.getFileContent()
|
|
62317
63067
|
});
|
|
62318
|
-
const
|
|
63068
|
+
const permissionConfig = isRecord$1(config.permission) ? config.permission : {};
|
|
63069
|
+
const { permission, nativeRules } = preserveKimiCodeRules(permissionConfig.rules);
|
|
62319
63070
|
const defaultPermissionMode = config.default_permission_mode;
|
|
63071
|
+
const dangerousCommandGuard = permissionConfig.dangerous_command_guard;
|
|
62320
63072
|
const tools = buildKimiCodeToolsSection(config.tools);
|
|
62321
63073
|
const toolOverride = {
|
|
62322
63074
|
...defaultPermissionMode === "manual" || defaultPermissionMode === "yolo" || defaultPermissionMode === "auto" ? { defaultPermissionMode } : {},
|
|
63075
|
+
...typeof dangerousCommandGuard === "boolean" && { dangerousCommandGuard },
|
|
62323
63076
|
...nativeRules.length > 0 && { rules: nativeRules },
|
|
62324
63077
|
...tools && { tools }
|
|
62325
63078
|
};
|
|
@@ -63201,6 +63954,7 @@ var MimocodePermissions = class extends OpencodePermissions {
|
|
|
63201
63954
|
//#endregion
|
|
63202
63955
|
//#region src/features/permissions/omp-permissions.ts
|
|
63203
63956
|
const CATCH_ALL_PATTERN$4 = "*";
|
|
63957
|
+
const APPROVAL_MODE_KEY = "approvalMode";
|
|
63204
63958
|
const ACTION_TO_OMP_APPROVAL = {
|
|
63205
63959
|
allow: "allow",
|
|
63206
63960
|
ask: "prompt",
|
|
@@ -63256,10 +64010,14 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
|
|
|
63256
64010
|
* commands, and its deny / ask also restrict every other tool's policy (a
|
|
63257
64011
|
* catch-all one at its own strictness, a pattern-specific one as `prompt`).
|
|
63258
64012
|
*
|
|
64013
|
+
* The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
|
|
64014
|
+
* `write` or `yolo`); without it the key is left as it is in the file, and on
|
|
64015
|
+
* import a recognized value is read back into the override.
|
|
64016
|
+
*
|
|
63259
64017
|
* `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
|
|
63260
|
-
* (the keys rulesync manages)
|
|
63261
|
-
*
|
|
63262
|
-
* never deleted.
|
|
64018
|
+
* (the keys rulesync manages), `bash.patterns` and an authored
|
|
64019
|
+
* `tools.approvalMode` are rewritten; sibling keys such as `bash.enabled` are
|
|
64020
|
+
* kept, and the file is never deleted.
|
|
63263
64021
|
*
|
|
63264
64022
|
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
|
|
63265
64023
|
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
|
|
@@ -63302,16 +64060,19 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
|
|
|
63302
64060
|
fileContent: existingContent,
|
|
63303
64061
|
filePath
|
|
63304
64062
|
});
|
|
64063
|
+
const config = rulesyncPermissions.getJson();
|
|
63305
64064
|
const { approval, patterns, floor } = convertRulesyncToOmp({
|
|
63306
|
-
config
|
|
64065
|
+
config,
|
|
63307
64066
|
logger
|
|
63308
64067
|
});
|
|
64068
|
+
const approvalMode = config.omp?.approvalMode;
|
|
63309
64069
|
const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
|
|
63310
64070
|
const nextApproval = {
|
|
63311
64071
|
...isRecord$1(existingTools.approval) ? Object.fromEntries(Object.entries(existingTools.approval).filter(([tool]) => !MANAGED_OMP_TOOLS.has(tool) && !Object.hasOwn(approval, tool)).map(([tool, value]) => [tool, raisedApproval(value, floor)])) : {},
|
|
63312
64072
|
...approval
|
|
63313
64073
|
};
|
|
63314
64074
|
const toolsSiblings = withoutKey(existingTools, "approval");
|
|
64075
|
+
if (approvalMode !== void 0) toolsSiblings[APPROVAL_MODE_KEY] = approvalMode;
|
|
63315
64076
|
const nextTools = Object.keys(nextApproval).length > 0 ? {
|
|
63316
64077
|
...toolsSiblings,
|
|
63317
64078
|
approval: nextApproval
|
|
@@ -63567,7 +64328,14 @@ function convertOmpToRulesync(config) {
|
|
|
63567
64328
|
permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
|
|
63568
64329
|
}
|
|
63569
64330
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
63570
|
-
|
|
64331
|
+
const approvalMode = tools[APPROVAL_MODE_KEY];
|
|
64332
|
+
return {
|
|
64333
|
+
permission,
|
|
64334
|
+
...isOmpApprovalMode(approvalMode) && { omp: { approvalMode } }
|
|
64335
|
+
};
|
|
64336
|
+
}
|
|
64337
|
+
function isOmpApprovalMode(value) {
|
|
64338
|
+
return OMP_APPROVAL_MODES.some((mode) => mode === value);
|
|
63571
64339
|
}
|
|
63572
64340
|
//#endregion
|
|
63573
64341
|
//#region src/features/permissions/pi-permissions.ts
|
|
@@ -64607,6 +65375,13 @@ const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
|
64607
65375
|
"allowPrivateNetworkHooks",
|
|
64608
65376
|
"allowedInsecureVoiceBaseUrls"
|
|
64609
65377
|
];
|
|
65378
|
+
const QWEN_OVERRIDE_SKILLS_KEYS = [
|
|
65379
|
+
"disabled",
|
|
65380
|
+
"defaultDisabled",
|
|
65381
|
+
"enabled",
|
|
65382
|
+
"disabledLevels"
|
|
65383
|
+
];
|
|
65384
|
+
const QWEN_OVERRIDE_GENERAL_KEYS = ["outputStyle"];
|
|
64610
65385
|
/**
|
|
64611
65386
|
* What each rule means for the two directions, so a key's behavior is declared
|
|
64612
65387
|
* once beside its rule rather than spelled out at every branch.
|
|
@@ -64663,77 +65438,125 @@ const QWEN_SCOPE_RULES = {
|
|
|
64663
65438
|
globalNote: "This is not a key rulesync models, so it cannot say what Qwen Code does with it — and some settings in these groups are potent (`tools.discoveryCommand` and `tools.callCommand` are spawned as commands when Qwen Code starts). Check what it now says for every project on this machine."
|
|
64664
65439
|
}
|
|
64665
65440
|
};
|
|
64666
|
-
const
|
|
64667
|
-
|
|
64668
|
-
|
|
64669
|
-
|
|
64670
|
-
|
|
64671
|
-
|
|
64672
|
-
|
|
64673
|
-
|
|
64674
|
-
|
|
64675
|
-
},
|
|
64676
|
-
|
|
64677
|
-
|
|
64678
|
-
|
|
64679
|
-
|
|
64680
|
-
|
|
64681
|
-
|
|
64682
|
-
|
|
64683
|
-
|
|
64684
|
-
|
|
64685
|
-
|
|
64686
|
-
|
|
64687
|
-
|
|
64688
|
-
|
|
64689
|
-
|
|
64690
|
-
|
|
64691
|
-
|
|
64692
|
-
},
|
|
64693
|
-
|
|
64694
|
-
|
|
64695
|
-
|
|
64696
|
-
|
|
64697
|
-
},
|
|
64698
|
-
|
|
64699
|
-
|
|
64700
|
-
|
|
64701
|
-
|
|
64702
|
-
},
|
|
64703
|
-
|
|
64704
|
-
|
|
64705
|
-
|
|
64706
|
-
|
|
64707
|
-
},
|
|
64708
|
-
|
|
64709
|
-
|
|
64710
|
-
|
|
64711
|
-
|
|
64712
|
-
},
|
|
64713
|
-
|
|
64714
|
-
|
|
64715
|
-
|
|
64716
|
-
|
|
64717
|
-
},
|
|
64718
|
-
|
|
65441
|
+
const QWEN_SCOPED_TOOLS_KEYS = {
|
|
65442
|
+
workflowsEnabled: { rule: "workspace-stripped" },
|
|
65443
|
+
executionSandbox: {
|
|
65444
|
+
rule: "workspace-stripped",
|
|
65445
|
+
grants: () => true,
|
|
65446
|
+
globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot choose its own sandbox; in the global scope it sets the filesystem and network policy every tool command runs under for every project on this machine (a System value still replaces it, and Qwen Code needs a restart to pick it up)."
|
|
65447
|
+
},
|
|
65448
|
+
workflowNameOnly: {
|
|
65449
|
+
rule: "global-machine-wide",
|
|
65450
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code lets a workspace only turn this key on: \`true\` restricts the model to named workflows in this repository, while any other value cannot lift a restriction set in user or system settings.`,
|
|
65451
|
+
globalNote: "Qwen Code honors this key in the global scope for every project on this machine, and a repository can only add the restriction, never lift it, so this decides whether the model may run inline or path-based workflow scripts anywhere a workspace does not turn the restriction on itself."
|
|
65452
|
+
},
|
|
65453
|
+
approvalMode: {
|
|
65454
|
+
rule: "global-machine-wide",
|
|
65455
|
+
projectNote: autonomyProjectNote
|
|
65456
|
+
},
|
|
65457
|
+
autoAccept: {
|
|
65458
|
+
rule: "global-machine-wide",
|
|
65459
|
+
projectNote: autonomyProjectNote
|
|
65460
|
+
},
|
|
65461
|
+
sandbox: {
|
|
65462
|
+
rule: "global-machine-wide",
|
|
65463
|
+
projectNote: autonomyProjectNote
|
|
65464
|
+
},
|
|
65465
|
+
sandboxImage: {
|
|
65466
|
+
rule: "global-machine-wide",
|
|
65467
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so a sandboxed run in this repository executes inside the named image — check that it is one you trust to run your code.`,
|
|
65468
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope every sandboxed run on this machine executes inside the named image — check that it is one you trust to run your code."
|
|
65469
|
+
},
|
|
65470
|
+
disabled: {
|
|
65471
|
+
rule: "global-machine-wide",
|
|
65472
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so a name that is gone is a tool the model can call again in this repository unless another scope still disables it.`,
|
|
65473
|
+
globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope this decides which tools stay out of the registry for every project on this machine — dropping a name hands that tool back to the model unless another scope still disables it."
|
|
65474
|
+
},
|
|
65475
|
+
visible: {
|
|
65476
|
+
rule: "global-machine-wide",
|
|
65477
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides which deferred tools are visible at startup in this repository.`,
|
|
65478
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides which deferred tools are visible at startup for every project on this machine."
|
|
65479
|
+
},
|
|
65480
|
+
eager: {
|
|
65481
|
+
rule: "global-machine-wide",
|
|
65482
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so in this repository every non-exempt eager-by-default tool it does not name is demoted to deferred — still callable, but only after \`tool_search\` loads it, and out of reach for the session if \`tool_search\` is not registered. A name added back is a tool whose schema is in the initial request again.`,
|
|
65483
|
+
globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope it decides which eager-by-default tools keep their schemas in the initial request for every project on this machine — the ones it does not name are demoted to deferred, reachable only through `tool_search`, and out of reach entirely for a session where `tool_search` is not registered, while a name it adds back is a tool declared up front again."
|
|
65484
|
+
},
|
|
65485
|
+
listDirectory: {
|
|
65486
|
+
rule: "global-machine-wide",
|
|
65487
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
|
|
65488
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `list_directory` tool is registered for every project on this machine."
|
|
65489
|
+
},
|
|
65490
|
+
todoWrite: {
|
|
65491
|
+
rule: "global-machine-wide",
|
|
65492
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`todo_write\` tool is registered in this repository.`,
|
|
65493
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
|
|
65494
|
+
}
|
|
65495
|
+
};
|
|
65496
|
+
/**
|
|
65497
|
+
* The notes for a skills list that hides something. Qwen Code unions the list
|
|
65498
|
+
* across scopes, but the override replaces the one in the file it writes, so
|
|
65499
|
+
* the consequence worth naming is what dropping an entry hands back.
|
|
65500
|
+
*/
|
|
65501
|
+
function skillsHidingListNotes(hides) {
|
|
65502
|
+
return {
|
|
65503
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so it decides which ${hides} in this repository — an entry that is gone is one the model can load again unless another scope still names it.`,
|
|
65504
|
+
globalNote: `Qwen Code honors this key wherever it is written and unions it across scopes, but the override replaces the list in this file rather than adding to it, so in the global scope this decides which ${hides} for every project on this machine — an entry that is gone is one the model can load again unless another scope still names it.`
|
|
65505
|
+
};
|
|
65506
|
+
}
|
|
65507
|
+
const QWEN_SCOPED_SKILLS_KEYS = {
|
|
65508
|
+
disabled: {
|
|
65509
|
+
rule: "global-machine-wide",
|
|
65510
|
+
...skillsHidingListNotes("skills are hidden")
|
|
65511
|
+
},
|
|
65512
|
+
defaultDisabled: {
|
|
65513
|
+
rule: "global-machine-wide",
|
|
65514
|
+
...skillsHidingListNotes("skills start disabled until `skills.enabled` opts into them")
|
|
65515
|
+
},
|
|
65516
|
+
disabledLevels: {
|
|
65517
|
+
rule: "global-machine-wide",
|
|
65518
|
+
...skillsHidingListNotes("skill discovery levels (`project`, `user`, `extension`, `bundled`) are skipped")
|
|
65519
|
+
},
|
|
65520
|
+
enabled: {
|
|
65521
|
+
rule: "global-machine-wide",
|
|
65522
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so in this repository it opts the model into the skills it names that \`skills.defaultDisabled\` would otherwise keep off in any scope (it cannot override \`skills.disabled\` or a skipped \`skills.disabledLevels\` level). Check it if the override arrived with a repository you cloned.`,
|
|
65523
|
+
globalNote: "Qwen Code honors this key wherever it is written and unions it across scopes, so in the global scope it opts the model into the skills it names that `skills.defaultDisabled` would otherwise keep off, for every project on this machine (it cannot override `skills.disabled` or a skipped `skills.disabledLevels` level)."
|
|
65524
|
+
}
|
|
65525
|
+
};
|
|
65526
|
+
const QWEN_OVERRIDE_GROUPS = [
|
|
65527
|
+
{
|
|
65528
|
+
groupName: "tools",
|
|
65529
|
+
overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
|
|
65530
|
+
scopedKeys: QWEN_SCOPED_TOOLS_KEYS
|
|
65531
|
+
},
|
|
65532
|
+
{
|
|
65533
|
+
groupName: "security",
|
|
65534
|
+
overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
|
|
65535
|
+
scopedKeys: {
|
|
65536
|
+
allowPrivateNetworkHooks: { rule: "workspace-stripped" },
|
|
65537
|
+
allowedInsecureVoiceBaseUrls: {
|
|
65538
|
+
rule: "workspace-stripped",
|
|
65539
|
+
grants: isNonEmptyArray
|
|
65540
|
+
},
|
|
65541
|
+
allowedHttpHookUrls: { rule: "workspace-non-overriding" },
|
|
65542
|
+
folderTrust: { rule: "user-scope-trust-check" }
|
|
65543
|
+
}
|
|
65544
|
+
},
|
|
65545
|
+
{
|
|
65546
|
+
groupName: "skills",
|
|
65547
|
+
overrideKeys: QWEN_OVERRIDE_SKILLS_KEYS,
|
|
65548
|
+
scopedKeys: QWEN_SCOPED_SKILLS_KEYS
|
|
65549
|
+
},
|
|
65550
|
+
{
|
|
65551
|
+
groupName: "general",
|
|
65552
|
+
overrideKeys: QWEN_OVERRIDE_GENERAL_KEYS,
|
|
65553
|
+
scopedKeys: { outputStyle: {
|
|
64719
65554
|
rule: "global-machine-wide",
|
|
64720
|
-
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it
|
|
64721
|
-
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope
|
|
64722
|
-
}
|
|
64723
|
-
}
|
|
64724
|
-
}, {
|
|
64725
|
-
groupName: "security",
|
|
64726
|
-
overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
|
|
64727
|
-
scopedKeys: {
|
|
64728
|
-
allowPrivateNetworkHooks: { rule: "workspace-stripped" },
|
|
64729
|
-
allowedInsecureVoiceBaseUrls: {
|
|
64730
|
-
rule: "workspace-stripped",
|
|
64731
|
-
grants: isNonEmptyArray
|
|
64732
|
-
},
|
|
64733
|
-
allowedHttpHookUrls: { rule: "workspace-non-overriding" },
|
|
64734
|
-
folderTrust: { rule: "user-scope-trust-check" }
|
|
65555
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it picks the output style for this repository. A custom style adds the instructions in its style file to the system prompt and can drop the built-in coding guidance (\`keep-coding-instructions\`) — a project style file is read only when the workspace is trusted. Check it if the override arrived with a repository you cloned.`,
|
|
65556
|
+
globalNote: "Qwen Code honors this key wherever it is written, so in the global scope it picks the output style for every project on this machine that does not choose its own. A custom style adds the instructions in its style file to the system prompt and can drop the built-in coding guidance (`keep-coding-instructions`)."
|
|
65557
|
+
} }
|
|
64735
65558
|
}
|
|
64736
|
-
|
|
65559
|
+
];
|
|
64737
65560
|
const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
|
|
64738
65561
|
const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
|
|
64739
65562
|
rule: "global-machine-wide",
|
|
@@ -64864,7 +65687,7 @@ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGrou
|
|
|
64864
65687
|
return scoped;
|
|
64865
65688
|
}
|
|
64866
65689
|
/**
|
|
64867
|
-
* Build the `tools`/`security` patch groups contributed by the `qwencode`
|
|
65690
|
+
* Build the `tools`/`security`/`skills`/`general` patch groups contributed by the `qwencode`
|
|
64868
65691
|
* override. Each group is shallow-merged over what `settings.json` already has,
|
|
64869
65692
|
* after the scope gate has handled the keys Qwen Code honors in one scope only.
|
|
64870
65693
|
*/
|
|
@@ -65033,7 +65856,9 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
65033
65856
|
});
|
|
65034
65857
|
const overrideGroups = {
|
|
65035
65858
|
tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
|
|
65036
|
-
security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
|
|
65859
|
+
security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS),
|
|
65860
|
+
skills: pickQwenOverrideKeys(settings.skills, QWEN_OVERRIDE_SKILLS_KEYS),
|
|
65861
|
+
general: pickQwenOverrideKeys(settings.general, QWEN_OVERRIDE_GENERAL_KEYS)
|
|
65037
65862
|
};
|
|
65038
65863
|
if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
|
|
65039
65864
|
const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
|
|
@@ -65336,6 +66161,37 @@ function applyAllowDynamicBash({ permissions, authored }) {
|
|
|
65336
66161
|
reason: "lets an Allow fallback, Auto included, run the nested and indirect Bash that otherwise needs a human or an exact-literal grant — command and process substitution, a dynamic command name, 'eval', 'source', 'sh -c' and their kind"
|
|
65337
66162
|
}];
|
|
65338
66163
|
}
|
|
66164
|
+
/** The writer fallbacks `[permissions] mode` accepts. */
|
|
66165
|
+
const REASONIX_PERMISSION_MODES = /* @__PURE__ */ new Set([
|
|
66166
|
+
"ask",
|
|
66167
|
+
"allow",
|
|
66168
|
+
"deny"
|
|
66169
|
+
]);
|
|
66170
|
+
/**
|
|
66171
|
+
* Writes the override's `mode` into `[permissions]`. Reasonix honors the key
|
|
66172
|
+
* only in the user config and ignores a project value as user-scope only, so a
|
|
66173
|
+
* project-scope generate skips it with a warning and leaves whatever the file
|
|
66174
|
+
* had. A global `allow` is reported when it is newly written: it lets every
|
|
66175
|
+
* writer call that no rule matches run without a prompt.
|
|
66176
|
+
*
|
|
66177
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
66178
|
+
*/
|
|
66179
|
+
function applyPermissionsMode({ permissions, authored, global, relativeFilePath, logger }) {
|
|
66180
|
+
if (authored === void 0) return [];
|
|
66181
|
+
if (!global) {
|
|
66182
|
+
logger?.warn(`Reasonix permissions: skipping reasonix.mode = ${quoteValueForWarning(authored)} for the project-scoped ${relativeFilePath}; Reasonix honors [permissions] mode only in the user config and ignores a project value, so generate it with --global instead.`);
|
|
66183
|
+
return [];
|
|
66184
|
+
}
|
|
66185
|
+
const previous = permissions.mode;
|
|
66186
|
+
permissions.mode = authored;
|
|
66187
|
+
const normalize = (value) => typeof value === "string" ? value.trim().toLowerCase() : void 0;
|
|
66188
|
+
const mode = normalize(authored);
|
|
66189
|
+
if (mode === "ask" || mode === "deny" || normalize(previous) === mode) return [];
|
|
66190
|
+
return [{
|
|
66191
|
+
label: "permissions.mode",
|
|
66192
|
+
reason: "lets every writer call that no rule matches run without a prompt"
|
|
66193
|
+
}];
|
|
66194
|
+
}
|
|
65339
66195
|
function parseReasonixConfig(fileContent) {
|
|
65340
66196
|
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
65341
66197
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -65386,7 +66242,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
65386
66242
|
relativeDirPath: paths.relativeDirPath,
|
|
65387
66243
|
relativeFilePath: paths.relativeFilePath,
|
|
65388
66244
|
fileContent,
|
|
65389
|
-
validate
|
|
66245
|
+
validate,
|
|
66246
|
+
global
|
|
65390
66247
|
});
|
|
65391
66248
|
}
|
|
65392
66249
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
@@ -65441,6 +66298,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
65441
66298
|
permissions: mergedPermissions,
|
|
65442
66299
|
authored: override?.allowDynamicBash
|
|
65443
66300
|
});
|
|
66301
|
+
trustAffecting.push(...applyPermissionsMode({
|
|
66302
|
+
permissions: mergedPermissions,
|
|
66303
|
+
authored: override?.mode,
|
|
66304
|
+
global,
|
|
66305
|
+
relativeFilePath: relativeFilePathForLog,
|
|
66306
|
+
logger
|
|
66307
|
+
}));
|
|
65444
66308
|
const patch = { permissions: mergedPermissions };
|
|
65445
66309
|
if (override?.sandbox !== void 0) {
|
|
65446
66310
|
const mergedSandbox = {
|
|
@@ -65482,7 +66346,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
65482
66346
|
patch,
|
|
65483
66347
|
filePath
|
|
65484
66348
|
}),
|
|
65485
|
-
validate
|
|
66349
|
+
validate,
|
|
66350
|
+
global
|
|
65486
66351
|
});
|
|
65487
66352
|
}
|
|
65488
66353
|
toRulesyncPermissions() {
|
|
@@ -65507,6 +66372,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
65507
66372
|
const reasonixOverride = {};
|
|
65508
66373
|
const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
|
|
65509
66374
|
if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
|
|
66375
|
+
if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
|
|
65510
66376
|
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
65511
66377
|
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
65512
66378
|
if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
|
|
@@ -67072,10 +67938,15 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
67072
67938
|
outputRoot,
|
|
67073
67939
|
logger
|
|
67074
67940
|
});
|
|
67941
|
+
const inheritedProvider = global ? void 0 : await readGlobalTaktProvider({
|
|
67942
|
+
projectFilePath: filePath,
|
|
67943
|
+
logger
|
|
67944
|
+
});
|
|
67075
67945
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
67076
67946
|
const provider = resolveActiveProvider({
|
|
67077
67947
|
config,
|
|
67078
|
-
runtime: runtime.active ? runtime.config : void 0
|
|
67948
|
+
runtime: runtime.active ? runtime.config : void 0,
|
|
67949
|
+
inheritedProvider
|
|
67079
67950
|
});
|
|
67080
67951
|
const mode = deriveTaktPermissionMode(rulesyncJson);
|
|
67081
67952
|
const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
|
|
@@ -67185,10 +68056,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
67185
68056
|
* `provider_profiles` (unless that key is the pre-0.68.0 `claude` fallback),
|
|
67186
68057
|
* else the `claude-sdk` default (Takt 0.68.0+).
|
|
67187
68058
|
*/
|
|
67188
|
-
function resolveActiveProvider({ config, runtime }) {
|
|
68059
|
+
function resolveActiveProvider({ config, runtime, inheritedProvider }) {
|
|
67189
68060
|
const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
|
|
67190
68061
|
if (fromRuntime !== void 0) return fromRuntime;
|
|
67191
|
-
|
|
68062
|
+
const local = readProviderName(config);
|
|
68063
|
+
if (local !== void 0) return local;
|
|
68064
|
+
if (inheritedProvider !== void 0) return inheritedProvider;
|
|
67192
68065
|
const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
|
|
67193
68066
|
if (isPlainObject$1(profiles)) {
|
|
67194
68067
|
const keys = Object.keys(profiles);
|
|
@@ -67196,6 +68069,43 @@ function resolveActiveProvider({ config, runtime }) {
|
|
|
67196
68069
|
}
|
|
67197
68070
|
return TAKT_DEFAULT_PROVIDER;
|
|
67198
68071
|
}
|
|
68072
|
+
/**
|
|
68073
|
+
* The top-level `provider:` of a Takt `config.yaml`, when it names one: either
|
|
68074
|
+
* the plain name or the block form `provider: { type: <name>, ... }` Takt also
|
|
68075
|
+
* accepts (`ConfigProviderReference`).
|
|
68076
|
+
*/
|
|
68077
|
+
function readProviderName(config) {
|
|
68078
|
+
const raw = config[TAKT_PROVIDER_KEY];
|
|
68079
|
+
const provider = isPlainObject$1(raw) ? raw.type : raw;
|
|
68080
|
+
return typeof provider === "string" && provider.trim() !== "" ? provider : void 0;
|
|
68081
|
+
}
|
|
68082
|
+
/**
|
|
68083
|
+
* The top-level `provider:` of the global `~/.takt/config.yaml`, read for a
|
|
68084
|
+
* project-scope generate. An unresolvable home directory, a missing or
|
|
68085
|
+
* unparsable file, or one that names no provider all mean "nothing inherited".
|
|
68086
|
+
*/
|
|
68087
|
+
async function readGlobalTaktProvider({ projectFilePath, logger }) {
|
|
68088
|
+
let globalPath;
|
|
68089
|
+
try {
|
|
68090
|
+
globalPath = join(getHomeDirectory(), TAKT_DIR, TAKT_CONFIG_FILE_NAME);
|
|
68091
|
+
} catch {
|
|
68092
|
+
return;
|
|
68093
|
+
}
|
|
68094
|
+
if (globalPath === projectFilePath) return;
|
|
68095
|
+
try {
|
|
68096
|
+
const content = await readFileContentOrNull(globalPath);
|
|
68097
|
+
if (content === null) return;
|
|
68098
|
+
return readProviderName(parseSharedConfig({
|
|
68099
|
+
format: "yaml",
|
|
68100
|
+
fileContent: content,
|
|
68101
|
+
filePath: globalPath,
|
|
68102
|
+
invalidRootPolicy: "error"
|
|
68103
|
+
}));
|
|
68104
|
+
} catch (error) {
|
|
68105
|
+
logger?.warn(`Takt permissions: could not read the provider from ${globalPath} (${formatError(error)}); falling back to the project config alone.`);
|
|
68106
|
+
return;
|
|
68107
|
+
}
|
|
68108
|
+
}
|
|
67199
68109
|
/** A plain object carrying at least one entry (Takt's "assignment" test). */
|
|
67200
68110
|
function hasEntries(value) {
|
|
67201
68111
|
return isPlainObject$1(value) && Object.keys(value).length > 0;
|
|
@@ -69226,6 +70136,9 @@ const WARP_OVERRIDE_KEYS = [
|
|
|
69226
70136
|
"agent_mode_execute_readonly_commands"
|
|
69227
70137
|
];
|
|
69228
70138
|
const WARP_EXECUTION_PROFILE_OVERRIDE_KEY = "execution_profile";
|
|
70139
|
+
const WARP_AUTO_APPROVE_BYPASS_KEY = "auto_approve_bypasses_command_denylist";
|
|
70140
|
+
const WARP_AGENT_KEY = "warp_agent";
|
|
70141
|
+
const WARP_AGENT_OTHER_KEY = "other";
|
|
69229
70142
|
const WARP_EXECUTION_PROFILE_KEYS = [
|
|
69230
70143
|
"read_files",
|
|
69231
70144
|
"apply_code_diffs",
|
|
@@ -69374,7 +70287,7 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
69374
70287
|
const override = config.warp;
|
|
69375
70288
|
const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
|
|
69376
70289
|
if (isRecord$1(override)) {
|
|
69377
|
-
const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
|
|
70290
|
+
const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, [WARP_AUTO_APPROVE_BYPASS_KEY]: _autoApproveBypass, ...legacyOverride } = override;
|
|
69378
70291
|
Object.assign(profiles, legacyOverride);
|
|
69379
70292
|
}
|
|
69380
70293
|
const mergedAllow = uniq(allow.toSorted());
|
|
@@ -69384,9 +70297,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
69384
70297
|
if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
|
|
69385
70298
|
else delete profiles[DENYLIST_KEY];
|
|
69386
70299
|
agents.profiles = profiles;
|
|
70300
|
+
applyAutoApproveBypassOverride({
|
|
70301
|
+
agents,
|
|
70302
|
+
override
|
|
70303
|
+
});
|
|
69387
70304
|
warnAboutDenylistReplacement({
|
|
69388
70305
|
toolLabel: "Warp",
|
|
70306
|
+
overrideKey: "warp",
|
|
69389
70307
|
denyCount: mergedDeny.length,
|
|
70308
|
+
autoApproveBypassesDenylist: readAutoApproveBypass(agents),
|
|
69390
70309
|
logger
|
|
69391
70310
|
});
|
|
69392
70311
|
mergeIntoDefaultExecutionProfile({
|
|
@@ -69425,6 +70344,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
69425
70344
|
for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
|
|
69426
70345
|
const executionProfileOverride = defaultProfile ? liftExecutionProfileOverride(defaultProfile) : void 0;
|
|
69427
70346
|
if (executionProfileOverride) warpOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
|
|
70347
|
+
const autoApproveBypass = readAutoApproveBypass(agents);
|
|
70348
|
+
if (autoApproveBypass !== void 0) warpOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
|
|
69428
70349
|
const result = { ...config };
|
|
69429
70350
|
if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
|
|
69430
70351
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
@@ -69448,12 +70369,40 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
69448
70369
|
};
|
|
69449
70370
|
/**
|
|
69450
70371
|
* Writing `command_denylist` at all replaces Warp's built-in default denylist,
|
|
69451
|
-
* so any non-empty list rulesync writes deserves a heads-up.
|
|
70372
|
+
* so any non-empty list rulesync writes deserves a heads-up. Unless
|
|
70373
|
+
* `auto_approve_bypasses_command_denylist` is `false`, auto-approve also runs
|
|
70374
|
+
* commands that match the list, so the warning says so.
|
|
69452
70375
|
* https://docs.warp.dev/agents/cli/permissions-and-profiles/
|
|
69453
70376
|
*/
|
|
69454
|
-
function warnAboutDenylistReplacement({ toolLabel, denyCount, logger }) {
|
|
70377
|
+
function warnAboutDenylistReplacement({ toolLabel, overrideKey, denyCount, autoApproveBypassesDenylist, logger }) {
|
|
69455
70378
|
if (denyCount === 0 || !logger) return;
|
|
69456
|
-
|
|
70379
|
+
const bypassNote = autoApproveBypassesDenylist === false ? "" : ` Auto-approve also runs commands that match the denylist unless ${overrideKey}.${WARP_AUTO_APPROVE_BYPASS_KEY} is set to false.`;
|
|
70380
|
+
logger.warn(`${toolLabel}'s command_denylist replaces its built-in default denylist, which covers rm, curl, wget, eval, ssh, shells, and other risky command patterns. The ${denyCount} deny rule(s) from .rulesync/permissions.jsonc are now the whole denylist — add equivalents for the built-in patterns you want to keep.${bypassNote}`);
|
|
70381
|
+
}
|
|
70382
|
+
/**
|
|
70383
|
+
* Write the override's `auto_approve_bypasses_command_denylist` into
|
|
70384
|
+
* `[agents.warp_agent.other]`, keeping every sibling key of `agents.warp_agent`
|
|
70385
|
+
* and of its `other` table. Nothing is written when the override omits it.
|
|
70386
|
+
*/
|
|
70387
|
+
function applyAutoApproveBypassOverride({ agents, override }) {
|
|
70388
|
+
if (!isRecord$1(override)) return;
|
|
70389
|
+
const value = override[WARP_AUTO_APPROVE_BYPASS_KEY];
|
|
70390
|
+
if (typeof value !== "boolean") return;
|
|
70391
|
+
const warpAgent = isRecord$1(agents[WARP_AGENT_KEY]) ? { ...agents[WARP_AGENT_KEY] } : {};
|
|
70392
|
+
const other = isRecord$1(warpAgent[WARP_AGENT_OTHER_KEY]) ? { ...warpAgent[WARP_AGENT_OTHER_KEY] } : {};
|
|
70393
|
+
other[WARP_AUTO_APPROVE_BYPASS_KEY] = value;
|
|
70394
|
+
warpAgent[WARP_AGENT_OTHER_KEY] = other;
|
|
70395
|
+
agents[WARP_AGENT_KEY] = warpAgent;
|
|
70396
|
+
}
|
|
70397
|
+
/**
|
|
70398
|
+
* Read `[agents.warp_agent.other] auto_approve_bypasses_command_denylist`, or
|
|
70399
|
+
* `undefined` when it is unset (Warp then defaults to `true`).
|
|
70400
|
+
*/
|
|
70401
|
+
function readAutoApproveBypass(agents) {
|
|
70402
|
+
const warpAgent = agents[WARP_AGENT_KEY];
|
|
70403
|
+
const other = isRecord$1(warpAgent) ? warpAgent[WARP_AGENT_OTHER_KEY] : void 0;
|
|
70404
|
+
const value = isRecord$1(other) ? other[WARP_AUTO_APPROVE_BYPASS_KEY] : void 0;
|
|
70405
|
+
return typeof value === "boolean" ? value : void 0;
|
|
69457
70406
|
}
|
|
69458
70407
|
/**
|
|
69459
70408
|
* On migrated installs the legacy `[agents.profiles]` keys are inert: runtime
|
|
@@ -69807,6 +70756,9 @@ const WARPCLI_GLOBAL_ONLY_MESSAGE = "Warp Agent CLI permissions are global-only;
|
|
|
69807
70756
|
* (`read_files`, `apply_code_diffs`, `run_agents`, ...), merged first so the
|
|
69808
70757
|
* rulesync-owned command lists always win.
|
|
69809
70758
|
*
|
|
70759
|
+
* The `warpcli.auto_approve_bypasses_command_denylist` override is written to
|
|
70760
|
+
* the sibling `[agents.warp_agent.other]` table.
|
|
70761
|
+
*
|
|
69810
70762
|
* Unlike the `warp` target, the CLI never read the app's legacy
|
|
69811
70763
|
* `[agents.profiles]` keys, so none are written, and there is no settings
|
|
69812
70764
|
* migration to wait for: the CLI's settings file "is created the first time you
|
|
@@ -69873,11 +70825,6 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
|
|
|
69873
70825
|
});
|
|
69874
70826
|
const mergedAllow = uniq(allow.toSorted());
|
|
69875
70827
|
const mergedDeny = uniq(deny.toSorted());
|
|
69876
|
-
warnAboutDenylistReplacement({
|
|
69877
|
-
toolLabel: "Warp Agent CLI",
|
|
69878
|
-
denyCount: mergedDeny.length,
|
|
69879
|
-
logger
|
|
69880
|
-
});
|
|
69881
70828
|
const override = config.warpcli;
|
|
69882
70829
|
const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
|
|
69883
70830
|
const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
|
|
@@ -69887,7 +70834,18 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
|
|
|
69887
70834
|
mergedDeny,
|
|
69888
70835
|
executionProfileOverride
|
|
69889
70836
|
});
|
|
70837
|
+
applyAutoApproveBypassOverride({
|
|
70838
|
+
agents,
|
|
70839
|
+
override
|
|
70840
|
+
});
|
|
69890
70841
|
settings.agents = agents;
|
|
70842
|
+
warnAboutDenylistReplacement({
|
|
70843
|
+
toolLabel: "Warp Agent CLI",
|
|
70844
|
+
overrideKey: "warpcli",
|
|
70845
|
+
denyCount: mergedDeny.length,
|
|
70846
|
+
autoApproveBypassesDenylist: readAutoApproveBypass(agents),
|
|
70847
|
+
logger
|
|
70848
|
+
});
|
|
69891
70849
|
return new WarpcliPermissions({
|
|
69892
70850
|
outputRoot,
|
|
69893
70851
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -69914,8 +70872,12 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
|
|
|
69914
70872
|
deny: isStringArray$2(denylist) ? denylist : []
|
|
69915
70873
|
});
|
|
69916
70874
|
const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
|
|
70875
|
+
const warpcliOverride = {};
|
|
70876
|
+
if (executionProfileOverride) warpcliOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
|
|
70877
|
+
const autoApproveBypass = readAutoApproveBypass(agents);
|
|
70878
|
+
if (autoApproveBypass !== void 0) warpcliOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
|
|
69917
70879
|
const result = { ...config };
|
|
69918
|
-
if (
|
|
70880
|
+
if (Object.keys(warpcliOverride).length > 0) result.warpcli = warpcliOverride;
|
|
69919
70881
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
69920
70882
|
}
|
|
69921
70883
|
validate() {
|
|
@@ -81989,9 +82951,8 @@ var CodebuddySubagent = class CodebuddySubagent extends ToolSubagent {
|
|
|
81989
82951
|
* Keys of the rulesync `codewhale` section copied into the profile. Codewhale
|
|
81990
82952
|
* parses agent profiles with `deny_unknown_fields`, so an unknown key would
|
|
81991
82953
|
* make the whole profile fail to load; anything else in the section is dropped
|
|
81992
|
-
* with a warning. `[tools]` and `[permissions]` are
|
|
81993
|
-
*
|
|
81994
|
-
* profile that tries to widen them.
|
|
82954
|
+
* with a warning. The `[tools]` and `[permissions]` tables are handled
|
|
82955
|
+
* separately (see `CODEWHALE_NARROWING_TABLES`).
|
|
81995
82956
|
*/
|
|
81996
82957
|
const CODEWHALE_SECTION_KEYS = [
|
|
81997
82958
|
"role_hint",
|
|
@@ -82012,6 +82973,48 @@ const CODEWHALE_SECTION_KEY_ALIASES = {
|
|
|
82012
82973
|
reasoning_effort: ["thinking", "reasoning"]
|
|
82013
82974
|
};
|
|
82014
82975
|
const CODEWHALE_SECTION_ALIAS_TO_KEY = new Map(Object.entries(CODEWHALE_SECTION_KEY_ALIASES).flatMap(([key, aliases]) => (aliases ?? []).map((alias) => [alias, key])));
|
|
82976
|
+
/**
|
|
82977
|
+
* The `[tools]` and `[permissions]` tables of an agent profile, with the only
|
|
82978
|
+
* values Codewhale accepts in each key. Codewhale rejects a profile that tries
|
|
82979
|
+
* to widen access through them (`reject_permission_expansion` in
|
|
82980
|
+
* `fleet/profile.rs`), and both tables deny unknown fields, so any other key
|
|
82981
|
+
* or value is dropped with a warning.
|
|
82982
|
+
*/
|
|
82983
|
+
const CODEWHALE_NARROWING_TABLES = {
|
|
82984
|
+
tools: { posture: (value) => typeof value === "string" && [
|
|
82985
|
+
"read-only",
|
|
82986
|
+
"readonly",
|
|
82987
|
+
"read_only"
|
|
82988
|
+
].includes(value) },
|
|
82989
|
+
permissions: {
|
|
82990
|
+
allow_shell: (value) => value === false,
|
|
82991
|
+
trust: (value) => value === false,
|
|
82992
|
+
approval_required: (value) => value === true
|
|
82993
|
+
}
|
|
82994
|
+
};
|
|
82995
|
+
/**
|
|
82996
|
+
* Keeps the narrowing-only entries of a `[tools]` / `[permissions]` table and
|
|
82997
|
+
* reports every other entry by its dotted path. A non-table value is dropped
|
|
82998
|
+
* as a whole.
|
|
82999
|
+
*/
|
|
83000
|
+
function filterNarrowingTable(tableName, value) {
|
|
83001
|
+
const allowed = CODEWHALE_NARROWING_TABLES[tableName] ?? {};
|
|
83002
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {
|
|
83003
|
+
kept: void 0,
|
|
83004
|
+
dropped: [tableName]
|
|
83005
|
+
};
|
|
83006
|
+
const kept = {};
|
|
83007
|
+
const dropped = [];
|
|
83008
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
83009
|
+
const normalized = typeof entry === "string" ? entry.trim() : entry;
|
|
83010
|
+
if (Object.hasOwn(allowed, key) && allowed[key]?.(normalized)) kept[key] = normalized;
|
|
83011
|
+
else dropped.push(`${tableName}.${key}`);
|
|
83012
|
+
}
|
|
83013
|
+
return {
|
|
83014
|
+
kept: Object.keys(kept).length > 0 ? kept : void 0,
|
|
83015
|
+
dropped
|
|
83016
|
+
};
|
|
83017
|
+
}
|
|
82015
83018
|
const CodewhaleSubagentTomlSchema = z.looseObject({
|
|
82016
83019
|
id: z.optional(z.string()),
|
|
82017
83020
|
name: z.optional(z.string()),
|
|
@@ -82038,7 +83041,8 @@ function stringifyCodewhaleSubagentToml(fields, instructions) {
|
|
|
82038
83041
|
*
|
|
82039
83042
|
* Generation writes `id` (the file stem), `display_name` (the rulesync name),
|
|
82040
83043
|
* `description`, and the body as `[instructions] text`, plus the allowlisted
|
|
82041
|
-
* keys of the rulesync `codewhale` section
|
|
83044
|
+
* keys of the rulesync `codewhale` section and its narrowing-only `[tools]` /
|
|
83045
|
+
* `[permissions]` entries.
|
|
82042
83046
|
*
|
|
82043
83047
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/SUBAGENTS.md
|
|
82044
83048
|
* @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/fleet/profile.rs
|
|
@@ -82073,6 +83077,10 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
|
|
|
82073
83077
|
const spelling = [key, ...CODEWHALE_SECTION_KEY_ALIASES[key] ?? []].find((candidate) => record[candidate] !== void 0);
|
|
82074
83078
|
if (spelling !== void 0) codewhaleSection[key] = record[spelling];
|
|
82075
83079
|
}
|
|
83080
|
+
for (const tableName of Object.keys(CODEWHALE_NARROWING_TABLES)) if (record[tableName] !== void 0) {
|
|
83081
|
+
const { kept } = filterNarrowingTable(tableName, record[tableName]);
|
|
83082
|
+
if (kept !== void 0) codewhaleSection[tableName] = kept;
|
|
83083
|
+
}
|
|
82076
83084
|
return new RulesyncSubagent({
|
|
82077
83085
|
outputRoot: ".",
|
|
82078
83086
|
frontmatter: {
|
|
@@ -82095,6 +83103,12 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
|
|
|
82095
83103
|
const droppedKeys = [];
|
|
82096
83104
|
const sectionSpellings = /* @__PURE__ */ new Map();
|
|
82097
83105
|
for (const [rawKey, value] of Object.entries(rawSection)) {
|
|
83106
|
+
if (Object.hasOwn(CODEWHALE_NARROWING_TABLES, rawKey)) {
|
|
83107
|
+
const { kept, dropped } = filterNarrowingTable(rawKey, value);
|
|
83108
|
+
if (kept !== void 0) sectionFields[rawKey] = kept;
|
|
83109
|
+
droppedKeys.push(...dropped);
|
|
83110
|
+
continue;
|
|
83111
|
+
}
|
|
82098
83112
|
const key = CODEWHALE_SECTION_ALIAS_TO_KEY.get(rawKey) ?? rawKey;
|
|
82099
83113
|
if (CODEWHALE_SECTION_KEYS.includes(key) && typeof value === "string") {
|
|
82100
83114
|
if (key === rawKey || sectionFields[key] === void 0) {
|
|
@@ -82104,7 +83118,7 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
|
|
|
82104
83118
|
} else droppedKeys.push(rawKey);
|
|
82105
83119
|
} else droppedKeys.push(rawKey);
|
|
82106
83120
|
}
|
|
82107
|
-
if (droppedKeys.length > 0) logger?.warn(`Dropping unsupported or duplicate codewhale subagent keys in ${rulesyncSubagent.getRelativeFilePath()}: ${droppedKeys.join(", ")}. Codewhale rejects agent profiles with unknown fields, non-string values there, or two spellings of one key; supported keys are ${CODEWHALE_SECTION_KEYS.join(", ")}, each a string (the canonical spelling wins over an alias).`);
|
|
83121
|
+
if (droppedKeys.length > 0) logger?.warn(`Dropping unsupported or duplicate codewhale subagent keys in ${rulesyncSubagent.getRelativeFilePath()}: ${droppedKeys.join(", ")}. Codewhale rejects agent profiles with unknown fields, non-string values there, [tools] / [permissions] values that would widen access, or two spellings of one key; supported keys are ${CODEWHALE_SECTION_KEYS.join(", ")}, each a string (the canonical spelling wins over an alias), plus the narrowing-only tools.posture = "read-only" and permissions.allow_shell = false, permissions.trust = false, permissions.approval_required = true.`);
|
|
82108
83122
|
const body = stringifyCodewhaleSubagentToml({
|
|
82109
83123
|
id: toProfileId(relativeFilePath),
|
|
82110
83124
|
display_name: frontmatter.name,
|
|
@@ -88601,6 +89615,99 @@ const AntigravityRuleFrontmatterSchema = z.looseObject({
|
|
|
88601
89615
|
function toGlobalRuleFileName(relativeFilePath) {
|
|
88602
89616
|
return relativeFilePath.split(/[\\/]/).join("-");
|
|
88603
89617
|
}
|
|
89618
|
+
/** The project rules directory both Antigravity targets share, as a POSIX path. */
|
|
89619
|
+
const PROJECT_RULES_DIR = `${ANTIGRAVITY_IDE_AGENTS_DIR}/rules`;
|
|
89620
|
+
/**
|
|
89621
|
+
* The subproject directory a rule is scoped to, or `undefined` when it goes to
|
|
89622
|
+
* the project-root `.agents/rules/`. Antigravity reads a `.agents/rules/`
|
|
89623
|
+
* directory in any subdirectory and loads it for files under that directory,
|
|
89624
|
+
* so a non-root rule carrying `agentsmd.subprojectPath` is written there. Project
|
|
89625
|
+
* scope only: there is no workspace tree to nest under in global scope.
|
|
89626
|
+
*
|
|
89627
|
+
* @see https://antigravity.google/docs/rules
|
|
89628
|
+
*/
|
|
89629
|
+
function resolveAntigravitySubprojectPath({ rulesyncRule, global }) {
|
|
89630
|
+
const { root, agentsmd } = rulesyncRule.getFrontmatter();
|
|
89631
|
+
const subprojectPath = !global && root !== true ? agentsmd?.subprojectPath : void 0;
|
|
89632
|
+
return subprojectPath && ![
|
|
89633
|
+
"",
|
|
89634
|
+
".",
|
|
89635
|
+
"./"
|
|
89636
|
+
].includes(subprojectPath) ? subprojectPath : void 0;
|
|
89637
|
+
}
|
|
89638
|
+
/**
|
|
89639
|
+
* File name of a rule in a subproject's `.agents/rules/`. Antigravity reads
|
|
89640
|
+
* only the direct children of a rules directory, so a nested rulesync rule
|
|
89641
|
+
* (`frontend/style.md`) is flattened (`frontend-style.md`), and the name keeps
|
|
89642
|
+
* its case so the IDE and the CLI write the same file and an imported
|
|
89643
|
+
* `codeStyle.md` is written back to the same place.
|
|
89644
|
+
*/
|
|
89645
|
+
function toNestedRuleFileName(relativeFilePath) {
|
|
89646
|
+
return toGlobalRuleFileName(relativeFilePath);
|
|
89647
|
+
}
|
|
89648
|
+
/** The `.agents/rules/` directory of a subproject. */
|
|
89649
|
+
function toAntigravityNestedRulesDirPath(subprojectPath) {
|
|
89650
|
+
return join(subprojectPath, ANTIGRAVITY_IDE_AGENTS_DIR, "rules");
|
|
89651
|
+
}
|
|
89652
|
+
/**
|
|
89653
|
+
* Split a nested rule directory such as `packages/api/.agents/rules/sub` into
|
|
89654
|
+
* the subproject (`packages/api`) and the folder inside its rules directory
|
|
89655
|
+
* (`sub`). Returns `undefined` for the project-root `.agents/rules/`, for a
|
|
89656
|
+
* path with no `.agents/rules` segment, and for one under a hidden directory —
|
|
89657
|
+
* another tool's output or a worktree, not a subproject.
|
|
89658
|
+
*/
|
|
89659
|
+
function extractAntigravitySubprojectPath(relativeDirPath) {
|
|
89660
|
+
const segments = toPosixPath(relativeDirPath).split("/");
|
|
89661
|
+
const index = segments.findIndex((segment, i) => segment === ".agents" && segments[i + 1] === "rules");
|
|
89662
|
+
if (index <= 0) return;
|
|
89663
|
+
const subprojectSegments = segments.slice(0, index);
|
|
89664
|
+
if (subprojectSegments.some((segment) => segment === "" || segment.startsWith("."))) return;
|
|
89665
|
+
return {
|
|
89666
|
+
subprojectPath: subprojectSegments.join("/"),
|
|
89667
|
+
innerDirPath: segments.slice(index + 2).join("/")
|
|
89668
|
+
};
|
|
89669
|
+
}
|
|
89670
|
+
/**
|
|
89671
|
+
* The `.agents/rules/` folders of project subdirectories. Only their direct
|
|
89672
|
+
* `.md` children are read, because Antigravity ignores files nested in
|
|
89673
|
+
* subfolders of a rules directory. The shared nested
|
|
89674
|
+
* scan ignores every hidden directory, which would skip `.agents` itself, so
|
|
89675
|
+
* only a `.agents/rules/` below another hidden directory is ignored here
|
|
89676
|
+
* (`extractAntigravitySubprojectPath` rejects one again on import). Import-only,
|
|
89677
|
+
* like every other nested scan: these files sit outside the rulesync-owned root
|
|
89678
|
+
* `.agents/rules/`, so `--delete` never sweeps them.
|
|
89679
|
+
*/
|
|
89680
|
+
function getAntigravityNestedFilePatterns() {
|
|
89681
|
+
return {
|
|
89682
|
+
include: [`**/${PROJECT_RULES_DIR}/*.md`],
|
|
89683
|
+
ignore: [
|
|
89684
|
+
`${PROJECT_RULES_DIR}/**`,
|
|
89685
|
+
`**/.*/**/${PROJECT_RULES_DIR}/**`,
|
|
89686
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
89687
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
89688
|
+
],
|
|
89689
|
+
ownedDirPath: PROJECT_RULES_DIR
|
|
89690
|
+
};
|
|
89691
|
+
}
|
|
89692
|
+
/**
|
|
89693
|
+
* Resolve where a non-root project rule file read from disk lives. A file
|
|
89694
|
+
* discovered by `getNestedFilePatterns` arrives as its own directory plus its
|
|
89695
|
+
* basename; it is re-split into the subproject's rules directory and the path
|
|
89696
|
+
* inside it, matching how the root `.agents/rules/` is read.
|
|
89697
|
+
*/
|
|
89698
|
+
function resolveAntigravityNonRootFile({ relativeDirPath, relativeFilePath, defaultDirPath }) {
|
|
89699
|
+
const nested = relativeDirPath === void 0 ? void 0 : extractAntigravitySubprojectPath(relativeDirPath);
|
|
89700
|
+
if (nested === void 0) return {
|
|
89701
|
+
relativeDirPath: defaultDirPath,
|
|
89702
|
+
relativeFilePath
|
|
89703
|
+
};
|
|
89704
|
+
return {
|
|
89705
|
+
relativeDirPath: toAntigravityNestedRulesDirPath(nested.subprojectPath),
|
|
89706
|
+
relativeFilePath: join(nested.innerDirPath, relativeFilePath)
|
|
89707
|
+
};
|
|
89708
|
+
}
|
|
89709
|
+
/** The targets a nested rule is imported with: the two that read the shared tree. */
|
|
89710
|
+
const ANTIGRAVITY_NESTED_RULE_TARGETS = ["antigravity-ide", "antigravity-cli"];
|
|
88604
89711
|
/**
|
|
88605
89712
|
* Converts a comma-separated globs string or array to an array of globs.
|
|
88606
89713
|
* @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
|
|
@@ -88788,12 +89895,17 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
|
|
|
88788
89895
|
});
|
|
88789
89896
|
}
|
|
88790
89897
|
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
|
|
88791
|
-
const
|
|
89898
|
+
const nonRoot = resolveAntigravityNonRootFile({
|
|
89899
|
+
relativeDirPath,
|
|
89900
|
+
relativeFilePath,
|
|
89901
|
+
defaultDirPath: paths.nonRoot.relativeDirPath
|
|
89902
|
+
});
|
|
89903
|
+
const relativePath = join(nonRoot.relativeDirPath, nonRoot.relativeFilePath);
|
|
88792
89904
|
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
88793
89905
|
const rule = new AntigravityCliRule({
|
|
88794
89906
|
outputRoot,
|
|
88795
|
-
relativeDirPath:
|
|
88796
|
-
relativeFilePath,
|
|
89907
|
+
relativeDirPath: nonRoot.relativeDirPath,
|
|
89908
|
+
relativeFilePath: nonRoot.relativeFilePath,
|
|
88797
89909
|
fileContent,
|
|
88798
89910
|
validate,
|
|
88799
89911
|
root: false,
|
|
@@ -88825,11 +89937,21 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
|
|
|
88825
89937
|
if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
|
|
88826
89938
|
const frontmatter = strategy.generateFrontmatter(normalizeStoredAntigravity(storedAntigravity), rulesyncFrontmatter);
|
|
88827
89939
|
const fileContent = stringifyFrontmatter(rulesyncRule.getBody(), frontmatter);
|
|
88828
|
-
if (!global)
|
|
88829
|
-
|
|
88830
|
-
|
|
88831
|
-
|
|
88832
|
-
|
|
89940
|
+
if (!global) {
|
|
89941
|
+
const subprojectPath = resolveAntigravitySubprojectPath({
|
|
89942
|
+
rulesyncRule,
|
|
89943
|
+
global
|
|
89944
|
+
});
|
|
89945
|
+
return new AntigravityCliRule({
|
|
89946
|
+
...params,
|
|
89947
|
+
...subprojectPath !== void 0 && {
|
|
89948
|
+
relativeDirPath: toAntigravityNestedRulesDirPath(subprojectPath),
|
|
89949
|
+
relativeFilePath: toNestedRuleFileName(params.relativeFilePath)
|
|
89950
|
+
},
|
|
89951
|
+
fileContent,
|
|
89952
|
+
global
|
|
89953
|
+
});
|
|
89954
|
+
}
|
|
88833
89955
|
return new AntigravityCliRule({
|
|
88834
89956
|
...params,
|
|
88835
89957
|
relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
|
|
@@ -88845,22 +89967,32 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
|
|
|
88845
89967
|
globs: [],
|
|
88846
89968
|
antigravity: frontmatter
|
|
88847
89969
|
};
|
|
89970
|
+
const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
|
|
88848
89971
|
return new RulesyncRule({
|
|
88849
89972
|
outputRoot: process.cwd(),
|
|
88850
89973
|
relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
|
|
88851
89974
|
relativeFilePath: this.getRelativeFilePath(),
|
|
88852
89975
|
frontmatter: {
|
|
88853
89976
|
root: false,
|
|
88854
|
-
targets: ["*"],
|
|
89977
|
+
targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
|
|
88855
89978
|
...rulesyncData,
|
|
88856
89979
|
antigravity: {
|
|
88857
89980
|
...rulesyncData.antigravity,
|
|
88858
89981
|
globs: frontmatter.globs ? parseGlobsString$1(frontmatter.globs) : void 0
|
|
88859
|
-
}
|
|
89982
|
+
},
|
|
89983
|
+
...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
|
|
88860
89984
|
},
|
|
88861
89985
|
body
|
|
88862
89986
|
});
|
|
88863
89987
|
}
|
|
89988
|
+
/**
|
|
89989
|
+
* The `.agents/rules/` folders of project subdirectories, imported back as
|
|
89990
|
+
* directory-scoped rules.
|
|
89991
|
+
* @see https://antigravity.google/docs/rules
|
|
89992
|
+
*/
|
|
89993
|
+
static getNestedFilePatterns() {
|
|
89994
|
+
return getAntigravityNestedFilePatterns();
|
|
89995
|
+
}
|
|
88864
89996
|
validate() {
|
|
88865
89997
|
if (this.root) return {
|
|
88866
89998
|
success: true,
|
|
@@ -88990,8 +90122,15 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
88990
90122
|
global
|
|
88991
90123
|
});
|
|
88992
90124
|
}
|
|
88993
|
-
const
|
|
88994
|
-
|
|
90125
|
+
const nonRoot = this.supportsSubprojectRules() ? resolveAntigravityNonRootFile({
|
|
90126
|
+
relativeDirPath,
|
|
90127
|
+
relativeFilePath,
|
|
90128
|
+
defaultDirPath: paths.nonRoot.relativeDirPath
|
|
90129
|
+
}) : {
|
|
90130
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
90131
|
+
relativeFilePath
|
|
90132
|
+
};
|
|
90133
|
+
const filePath = join(outputRoot, nonRoot.relativeDirPath, nonRoot.relativeFilePath);
|
|
88995
90134
|
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
88996
90135
|
let parsedFrontmatter;
|
|
88997
90136
|
if (validate) {
|
|
@@ -89001,8 +90140,8 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
89001
90140
|
} else parsedFrontmatter = frontmatter;
|
|
89002
90141
|
return new this({
|
|
89003
90142
|
outputRoot,
|
|
89004
|
-
relativeDirPath:
|
|
89005
|
-
relativeFilePath,
|
|
90143
|
+
relativeDirPath: nonRoot.relativeDirPath,
|
|
90144
|
+
relativeFilePath: nonRoot.relativeFilePath,
|
|
89006
90145
|
body,
|
|
89007
90146
|
frontmatter: parsedFrontmatter,
|
|
89008
90147
|
validate,
|
|
@@ -89010,6 +90149,25 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
89010
90149
|
global
|
|
89011
90150
|
});
|
|
89012
90151
|
}
|
|
90152
|
+
/**
|
|
90153
|
+
* Whether a rule scoped to a subproject goes to that subproject's own
|
|
90154
|
+
* `.agents/rules/`. A subclass that writes somewhere other than a project
|
|
90155
|
+
* tree (the plugin bundle) turns it off.
|
|
90156
|
+
*/
|
|
90157
|
+
static supportsSubprojectRules() {
|
|
90158
|
+
return true;
|
|
90159
|
+
}
|
|
90160
|
+
/**
|
|
90161
|
+
* The `.agents/rules/` folders of project subdirectories, imported back as
|
|
90162
|
+
* directory-scoped rules.
|
|
90163
|
+
* @see https://antigravity.google/docs/rules
|
|
90164
|
+
*/
|
|
90165
|
+
static getNestedFilePatterns() {
|
|
90166
|
+
return this.supportsSubprojectRules() ? getAntigravityNestedFilePatterns() : {
|
|
90167
|
+
include: [],
|
|
90168
|
+
ignore: []
|
|
90169
|
+
};
|
|
90170
|
+
}
|
|
89013
90171
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
89014
90172
|
const paths = this.getSettablePaths({ global });
|
|
89015
90173
|
if (rulesyncRule.getFrontmatter().root) {
|
|
@@ -89032,10 +90190,14 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
89032
90190
|
const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
|
|
89033
90191
|
if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
|
|
89034
90192
|
const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
|
|
89035
|
-
const
|
|
90193
|
+
const subprojectPath = this.supportsSubprojectRules() ? resolveAntigravitySubprojectPath({
|
|
90194
|
+
rulesyncRule,
|
|
90195
|
+
global
|
|
90196
|
+
}) : void 0;
|
|
90197
|
+
const relativeFilePath = global || subprojectPath !== void 0 ? toNestedRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
|
|
89036
90198
|
return new this({
|
|
89037
90199
|
outputRoot,
|
|
89038
|
-
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
90200
|
+
relativeDirPath: subprojectPath === void 0 ? paths.nonRoot.relativeDirPath : toAntigravityNestedRulesDirPath(subprojectPath),
|
|
89039
90201
|
relativeFilePath,
|
|
89040
90202
|
frontmatter,
|
|
89041
90203
|
body: rulesyncRule.getBody(),
|
|
@@ -89056,15 +90218,17 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
89056
90218
|
...rulesyncData.antigravity,
|
|
89057
90219
|
globs: this.frontmatter.globs ? parseGlobsString$1(this.frontmatter.globs) : void 0
|
|
89058
90220
|
};
|
|
90221
|
+
const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
|
|
89059
90222
|
return new RulesyncRule({
|
|
89060
90223
|
outputRoot: process.cwd(),
|
|
89061
90224
|
relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
|
|
89062
90225
|
relativeFilePath: this.getRelativeFilePath(),
|
|
89063
90226
|
frontmatter: {
|
|
89064
90227
|
root: false,
|
|
89065
|
-
targets: ["*"],
|
|
90228
|
+
targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
|
|
89066
90229
|
...rulesyncData,
|
|
89067
|
-
antigravity: antigravityForRulesync
|
|
90230
|
+
antigravity: antigravityForRulesync,
|
|
90231
|
+
...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
|
|
89068
90232
|
},
|
|
89069
90233
|
body: this.body
|
|
89070
90234
|
});
|
|
@@ -89118,6 +90282,9 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
89118
90282
|
//#endregion
|
|
89119
90283
|
//#region src/features/rules/antigravity-plugin-rule.ts
|
|
89120
90284
|
var AntigravityPluginRule = class extends AntigravityIdeRule {
|
|
90285
|
+
static supportsSubprojectRules() {
|
|
90286
|
+
return false;
|
|
90287
|
+
}
|
|
89121
90288
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
89122
90289
|
const targets = rulesyncRule.getFrontmatter().targets;
|
|
89123
90290
|
return targets.includes("*") || targets.includes("antigravity-plugin");
|
|
@@ -95467,6 +96634,12 @@ var VibeRule = class extends NestedAgentsmdRule {
|
|
|
95467
96634
|
* file, so every non-root rule without `agentsmd.subprojectPath` folds into
|
|
95468
96635
|
* the root `./AGENTS.md`.
|
|
95469
96636
|
*
|
|
96637
|
+
* A legacy root `WARP.md` is read on import when the root `AGENTS.md` is
|
|
96638
|
+
* absent, so a repository that has not moved to `AGENTS.md` yet imports its
|
|
96639
|
+
* rules. It is never written or deleted. When both exist, Warp reads
|
|
96640
|
+
* `WARP.md`, so import keeps reading the `AGENTS.md` rulesync manages and warns
|
|
96641
|
+
* that Warp ignores it, as generate already does.
|
|
96642
|
+
*
|
|
95470
96643
|
* In global mode, Warp reads a third rule source from `~/.agents/AGENTS.md`
|
|
95471
96644
|
* (the cross-tool agent config directory), indexed like project rules and also
|
|
95472
96645
|
* used from remote hosts in SSH sessions. The same root-fold policy applies.
|
|
@@ -95481,6 +96654,31 @@ var WarpRule = class extends NestedAgentsmdRule {
|
|
|
95481
96654
|
toolTarget: "warp"
|
|
95482
96655
|
};
|
|
95483
96656
|
}
|
|
96657
|
+
static getSettablePaths(options = {}) {
|
|
96658
|
+
const paths = super.getSettablePaths(options);
|
|
96659
|
+
if (options.global) return paths;
|
|
96660
|
+
return {
|
|
96661
|
+
...paths,
|
|
96662
|
+
importOnlyRoots: [{
|
|
96663
|
+
relativeDirPath: ".",
|
|
96664
|
+
relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
|
|
96665
|
+
onlyWhenRootAbsent: true,
|
|
96666
|
+
outranksRoot: true
|
|
96667
|
+
}]
|
|
96668
|
+
};
|
|
96669
|
+
}
|
|
96670
|
+
static async fromFile(params) {
|
|
96671
|
+
const { outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true } = params;
|
|
96672
|
+
if (!params.global && relativeFilePath === "WARP.md" && (relativeDirPath === void 0 || relativeDirPath === ".")) return new this({
|
|
96673
|
+
outputRoot,
|
|
96674
|
+
relativeDirPath: ".",
|
|
96675
|
+
relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
|
|
96676
|
+
fileContent: await readFileContent(join(outputRoot, WARP_LEGACY_RULE_FILE_NAME)),
|
|
96677
|
+
validate,
|
|
96678
|
+
root: true
|
|
96679
|
+
});
|
|
96680
|
+
return super.fromFile(params);
|
|
96681
|
+
}
|
|
95484
96682
|
};
|
|
95485
96683
|
//#endregion
|
|
95486
96684
|
//#region src/features/rules/zcode-rule.ts
|
|
@@ -96783,7 +97981,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
96783
97981
|
if (!importOnlyRoots || importOnlyRoots.length === 0) return;
|
|
96784
97982
|
const existingPaths = [];
|
|
96785
97983
|
for (const importOnlyRoot of importOnlyRoots) {
|
|
96786
|
-
if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
|
|
97984
|
+
if (importOnlyRoot.onlyWhenRootAbsent !== true || importOnlyRoot.outranksRoot === true) continue;
|
|
96787
97985
|
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
96788
97986
|
cwd: this.outputRoot,
|
|
96789
97987
|
type: "file"
|
|
@@ -97484,13 +98682,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
97484
98682
|
const rootFilePath = primaryRootFilePaths[0];
|
|
97485
98683
|
const scannedPaths = [];
|
|
97486
98684
|
const skippedPaths = [];
|
|
98685
|
+
const skippedOutrankingPaths = [];
|
|
97487
98686
|
for (const importOnlyRoot of importOnlyRoots) {
|
|
97488
98687
|
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
97489
98688
|
cwd: this.outputRoot,
|
|
97490
98689
|
type: "file"
|
|
97491
98690
|
});
|
|
97492
98691
|
if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
|
|
97493
|
-
skippedPaths.push(...matchedPaths);
|
|
98692
|
+
(importOnlyRoot.outranksRoot === true ? skippedOutrankingPaths : skippedPaths).push(...matchedPaths);
|
|
97494
98693
|
continue;
|
|
97495
98694
|
}
|
|
97496
98695
|
scannedPaths.push(...matchedPaths);
|
|
@@ -97501,6 +98700,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
97501
98700
|
const remainingCount = skippedNames.length - listedNames.length;
|
|
97502
98701
|
this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${stripControlCharacters(relative(this.outputRoot, rootFilePath))} exists, and the tool reads that file exclusively. Delete them once you have checked that content is in the root file, or move it into ${RULESYNC_RULES_RELATIVE_DIR_PATH} if it is not.`);
|
|
97503
98702
|
}
|
|
98703
|
+
if (skippedOutrankingPaths.length > 0 && rootFilePath !== void 0) {
|
|
98704
|
+
const rootName = stripControlCharacters(relative(this.outputRoot, rootFilePath));
|
|
98705
|
+
const skippedNames = skippedOutrankingPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
|
|
98706
|
+
const listedNames = skippedNames.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
98707
|
+
const remainingCount = skippedNames.length - listedNames.length;
|
|
98708
|
+
this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${rootName} is imported instead, but ${this.toolTarget} reads ${listedNames.length === 1 ? "that file" : "those files"} in place of ${rootName} when both exist. Move any content only it has into ${RULESYNC_RULES_RELATIVE_DIR_PATH} and delete it, so the generated ${rootName} takes effect.`);
|
|
98709
|
+
}
|
|
97504
98710
|
return await buildImportRulesFromPaths(scannedPaths);
|
|
97505
98711
|
})();
|
|
97506
98712
|
this.logger.debug(`Found ${importOnlyToolRules.length} import-only tool rule files`);
|
|
@@ -100321,4 +101527,4 @@ async function importChecksCore(params) {
|
|
|
100321
101527
|
//#endregion
|
|
100322
101528
|
export { RulesyncHooks as $, removeFile as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
|
|
100323
101529
|
|
|
100324
|
-
//# sourceMappingURL=import
|
|
101530
|
+
//# sourceMappingURL=import-ORYbMDJK.js.map
|