rulesync 16.9.1 → 16.11.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 +5 -3
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BaRDaZET.cjs → import--_2cfZlH.cjs} +1843 -164
- package/dist/{import-BpKoN2US.js → import-Bgkf_jVN.js} +1829 -150
- package/dist/import-Bgkf_jVN.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BpKoN2US.js.map +0 -1
|
@@ -134,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
|
|
|
134
134
|
};
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/constants/rulesync-paths.ts
|
|
137
|
-
const { join: join$
|
|
137
|
+
const { join: join$288 } = node_path.posix;
|
|
138
138
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
139
139
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
140
140
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
141
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
142
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
143
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
144
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
145
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
146
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
147
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
148
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
149
|
-
join$
|
|
150
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
151
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
141
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "rules");
|
|
142
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
|
|
143
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "commands");
|
|
144
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "subagents");
|
|
145
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "checks");
|
|
146
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
147
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
148
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
149
|
+
join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
150
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
151
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
152
152
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
153
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
153
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
154
154
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
155
155
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
156
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
157
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
156
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "skills");
|
|
157
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
|
|
158
158
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
159
159
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
160
160
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -196,6 +196,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
196
196
|
"kiro",
|
|
197
197
|
"kiro-cli",
|
|
198
198
|
"kiro-ide",
|
|
199
|
+
"musecode",
|
|
199
200
|
"opencode",
|
|
200
201
|
"pi",
|
|
201
202
|
"qwencode",
|
|
@@ -259,6 +260,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
259
260
|
"kiro-cli",
|
|
260
261
|
"kiro-ide",
|
|
261
262
|
"junie",
|
|
263
|
+
"musecode",
|
|
262
264
|
"opencode",
|
|
263
265
|
"qwencode",
|
|
264
266
|
"reasonix",
|
|
@@ -366,6 +368,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
366
368
|
"kiro",
|
|
367
369
|
"kiro-cli",
|
|
368
370
|
"kiro-ide",
|
|
371
|
+
"musecode",
|
|
369
372
|
"opencode",
|
|
370
373
|
"pi",
|
|
371
374
|
"qwencode",
|
|
@@ -420,6 +423,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
420
423
|
"cline",
|
|
421
424
|
"codexcli",
|
|
422
425
|
"copilot",
|
|
426
|
+
"copilotcli",
|
|
423
427
|
"cursor",
|
|
424
428
|
"devin",
|
|
425
429
|
"factorydroid",
|
|
@@ -443,6 +447,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
443
447
|
];
|
|
444
448
|
const checksProcessorToolTargetTuple = [
|
|
445
449
|
"amp",
|
|
450
|
+
"augmentcode",
|
|
446
451
|
"cursor",
|
|
447
452
|
"hermesagent",
|
|
448
453
|
"rovodev",
|
|
@@ -1755,6 +1760,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
1755
1760
|
".claude/settings.json",
|
|
1756
1761
|
".claude/settings.local.json",
|
|
1757
1762
|
".codex/config.toml",
|
|
1763
|
+
".copilot/settings.json",
|
|
1764
|
+
".github/copilot/settings.json",
|
|
1758
1765
|
".devin/config.json",
|
|
1759
1766
|
".factory/settings.json",
|
|
1760
1767
|
".grok/config.toml",
|
|
@@ -2494,14 +2501,23 @@ const CLINE_HOOK_EVENTS = [
|
|
|
2494
2501
|
/**
|
|
2495
2502
|
* Hook events supported by GitHub Copilot (cloud coding agent).
|
|
2496
2503
|
*
|
|
2497
|
-
*
|
|
2504
|
+
* The events rulesync writes to `.github/hooks/*.json`:
|
|
2498
2505
|
* `sessionStart`, `sessionEnd`, `userPromptSubmitted` ← `beforeSubmitPrompt`,
|
|
2499
|
-
* `preToolUse`, `postToolUse`, `agentStop` ← `stop`, `
|
|
2500
|
-
* `errorOccurred` ← `afterError
|
|
2501
|
-
* not part of the documented cloud-agent surface.
|
|
2506
|
+
* `preToolUse`, `postToolUse`, `agentStop` ← `stop`, `subagentStart`,
|
|
2507
|
+
* `subagentStop`, `errorOccurred` ← `afterError`, and `preCompact`.
|
|
2502
2508
|
*
|
|
2503
|
-
*
|
|
2504
|
-
*
|
|
2509
|
+
* `preCompact` and `subagentStart` are authorable because the unified hooks
|
|
2510
|
+
* reference's per-event "Cloud agent" column says both fire there. That column
|
|
2511
|
+
* is the authority for this set: the older cloud-agent concept page still
|
|
2512
|
+
* lists only the eight events this set began as, and re-narrowing to it would
|
|
2513
|
+
* undo that. `notification` and `permissionRequest` stay out because the same
|
|
2514
|
+
* column is explicit that they do not fire on the cloud agent.
|
|
2515
|
+
*
|
|
2516
|
+
* Two further CLI events — `postToolUseFailure` and `userPromptTransformed`
|
|
2517
|
+
* ({@link COPILOTCLI_HOOK_EVENTS}) — are documented as firing on the cloud
|
|
2518
|
+
* agent as well but are not modelled here yet.
|
|
2519
|
+
*
|
|
2520
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
2505
2521
|
*/
|
|
2506
2522
|
const COPILOT_HOOK_EVENTS = [
|
|
2507
2523
|
"sessionStart",
|
|
@@ -2510,8 +2526,10 @@ const COPILOT_HOOK_EVENTS = [
|
|
|
2510
2526
|
"preToolUse",
|
|
2511
2527
|
"postToolUse",
|
|
2512
2528
|
"stop",
|
|
2529
|
+
"subagentStart",
|
|
2513
2530
|
"subagentStop",
|
|
2514
|
-
"afterError"
|
|
2531
|
+
"afterError",
|
|
2532
|
+
"preCompact"
|
|
2515
2533
|
];
|
|
2516
2534
|
/**
|
|
2517
2535
|
* Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
|
|
@@ -2613,8 +2631,8 @@ const CODEXCLI_HOOK_EVENTS = [
|
|
|
2613
2631
|
* has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
|
|
2614
2632
|
* Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
|
|
2615
2633
|
* excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
|
|
2616
|
-
* @see https://github.com/
|
|
2617
|
-
* @see https://
|
|
2634
|
+
* @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
|
|
2635
|
+
* @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
|
|
2618
2636
|
*/
|
|
2619
2637
|
const GOOSE_HOOK_EVENTS = [
|
|
2620
2638
|
"sessionStart",
|
|
@@ -2651,8 +2669,8 @@ const KIRO_HOOK_EVENTS = [
|
|
|
2651
2669
|
* `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
|
|
2652
2670
|
* file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
|
|
2653
2671
|
* (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
|
|
2654
|
-
* those can still be emitted verbatim via
|
|
2655
|
-
*
|
|
2672
|
+
* those can still be emitted verbatim via the shared `kiro` override block
|
|
2673
|
+
* (unknown event keys pass through unchanged). There is no `SessionEnd`
|
|
2656
2674
|
* trigger, so the canonical `sessionEnd` has no home here.
|
|
2657
2675
|
* @see https://kiro.dev/docs/hooks/types/
|
|
2658
2676
|
*/
|
|
@@ -3247,8 +3265,10 @@ const CANONICAL_TO_COPILOT_EVENT_NAMES = {
|
|
|
3247
3265
|
preToolUse: "preToolUse",
|
|
3248
3266
|
postToolUse: "postToolUse",
|
|
3249
3267
|
stop: "agentStop",
|
|
3268
|
+
subagentStart: "subagentStart",
|
|
3250
3269
|
subagentStop: "subagentStop",
|
|
3251
|
-
afterError: "errorOccurred"
|
|
3270
|
+
afterError: "errorOccurred",
|
|
3271
|
+
preCompact: "preCompact"
|
|
3252
3272
|
};
|
|
3253
3273
|
/**
|
|
3254
3274
|
* Map Copilot camelCase event names to canonical camelCase.
|
|
@@ -3385,6 +3405,47 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
|
|
|
3385
3405
|
stop: "stop"
|
|
3386
3406
|
};
|
|
3387
3407
|
/**
|
|
3408
|
+
* Native event keys of the embedded agent-config hook format, as listed by the
|
|
3409
|
+
* Kiro CLI 3.0 migration guide's old-format column: `agentSpawn`,
|
|
3410
|
+
* `userPromptSubmit`, `preToolUse`, `postToolUse`, `fileEdited`, `fileCreated`
|
|
3411
|
+
* and `agentStop`/`stop`. Two of them (`fileEdited`, `fileCreated`) have no
|
|
3412
|
+
* canonical equivalent, so they exist only here.
|
|
3413
|
+
*
|
|
3414
|
+
* The `kiro` override block is shared with the standalone-format targets, whose
|
|
3415
|
+
* trigger vocabulary is different, so the alias writer uses this list to decide
|
|
3416
|
+
* what it can express.
|
|
3417
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
3418
|
+
*/
|
|
3419
|
+
const KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES = [
|
|
3420
|
+
"agentSpawn",
|
|
3421
|
+
"userPromptSubmit",
|
|
3422
|
+
"preToolUse",
|
|
3423
|
+
"postToolUse",
|
|
3424
|
+
"fileEdited",
|
|
3425
|
+
"fileCreated",
|
|
3426
|
+
"agentStop",
|
|
3427
|
+
"stop"
|
|
3428
|
+
];
|
|
3429
|
+
/**
|
|
3430
|
+
* Old agent-config event keys mapped to their standalone v1 trigger, so a
|
|
3431
|
+
* `kiro.hooks` block authored in the deprecated spelling still emits a valid
|
|
3432
|
+
* trigger for the `kiro-cli` / `kiro-ide` targets that read the same block.
|
|
3433
|
+
* The migration guide states the old names "map directly to their newer
|
|
3434
|
+
* equivalents" and spells out `agentSpawn` → `SessionStart` and `fileEdited` →
|
|
3435
|
+
* `PostFileSave`; the rest are the same event under the v1 casing.
|
|
3436
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
3437
|
+
*/
|
|
3438
|
+
const KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES = {
|
|
3439
|
+
agentSpawn: "SessionStart",
|
|
3440
|
+
userPromptSubmit: "UserPromptSubmit",
|
|
3441
|
+
preToolUse: "PreToolUse",
|
|
3442
|
+
postToolUse: "PostToolUse",
|
|
3443
|
+
fileEdited: "PostFileSave",
|
|
3444
|
+
fileCreated: "PostFileCreate",
|
|
3445
|
+
agentStop: "Stop",
|
|
3446
|
+
stop: "Stop"
|
|
3447
|
+
};
|
|
3448
|
+
/**
|
|
3388
3449
|
* Map Kiro CLI camelCase event names to canonical camelCase.
|
|
3389
3450
|
*/
|
|
3390
3451
|
const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
@@ -3392,8 +3453,8 @@ const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICA
|
|
|
3392
3453
|
* Map canonical camelCase event names to Kiro IDE PascalCase triggers.
|
|
3393
3454
|
*
|
|
3394
3455
|
* Only the canonical lifecycle events with a clean IDE equivalent are mapped.
|
|
3395
|
-
* Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via
|
|
3396
|
-
* override) pass through unchanged.
|
|
3456
|
+
* Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via the shared
|
|
3457
|
+
* `kiro` override block) pass through unchanged.
|
|
3397
3458
|
* @see https://kiro.dev/docs/hooks/types/
|
|
3398
3459
|
*/
|
|
3399
3460
|
const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
|
|
@@ -4894,13 +4955,57 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4894
4955
|
git_write_rules: zod_mini.z.optional(zod_mini.z.boolean())
|
|
4895
4956
|
});
|
|
4896
4957
|
/**
|
|
4958
|
+
* Tool-scoped override block for Zed. Two Zed surfaces sit outside the canonical
|
|
4959
|
+
* allow/ask/deny model that `agent.tool_permissions` implements, and both are
|
|
4960
|
+
* authored here verbatim:
|
|
4961
|
+
*
|
|
4962
|
+
* - `sandbox_permissions` — the OS-level agent sandbox, on by default since Zed
|
|
4963
|
+
* 1.14.2 for the `terminal` and `fetch` tools. Its defaults forbid network
|
|
4964
|
+
* access, writing outside the project directories, and writing to `.git`, so a
|
|
4965
|
+
* real setup usually needs to relax one of them (`network_hosts` with exact
|
|
4966
|
+
* hostnames or leading `*.` wildcards, `allow_all_hosts`, `write_paths`,
|
|
4967
|
+
* `allow_fs_write_all`, `allow_unsandboxed`). This is containment, not tool
|
|
4968
|
+
* gating: no canonical category expresses it, so the block is written straight
|
|
4969
|
+
* into `agent.sandbox_permissions`, mirroring the Kilo / Claude Code / Codex
|
|
4970
|
+
* CLI `sandbox` passthrough precedent. Kept a bare `looseObject` rather than
|
|
4971
|
+
* enumerating the keys, since upstream adds to them release over release.
|
|
4972
|
+
* - `profiles` — the tool-availability layer, a separate enforcement stage from
|
|
4973
|
+
* `tool_permissions`: a tool absent from the active profile cannot be used no
|
|
4974
|
+
* matter what the permission rules say. Per-profile keys are `name`, `tools`
|
|
4975
|
+
* (per-tool booleans), `enable_all_context_servers`, `context_servers` and
|
|
4976
|
+
* `default_model`. Like Kimi Code's `tools.enabled`/`disabled` block this is a
|
|
4977
|
+
* verbatim passthrough — no canonicalization is attempted.
|
|
4978
|
+
*
|
|
4979
|
+
* Neither surface can weaken a canonical deny, because `agent.tool_permissions`
|
|
4980
|
+
* is off-limits to this override: the translator consumes only the two keys
|
|
4981
|
+
* above by name and rejects a `tool_permissions` key with a warning.
|
|
4982
|
+
*
|
|
4983
|
+
* @see https://zed.dev/docs/ai/sandboxing
|
|
4984
|
+
* @see https://zed.dev/docs/ai/agent-profiles
|
|
4985
|
+
*
|
|
4986
|
+
* @example
|
|
4987
|
+
* { "sandbox_permissions": { "network_hosts": ["*.github.com"], "write_paths": ["/tmp"] },
|
|
4988
|
+
* "profiles": { "review": { "name": "Review", "tools": { "terminal": false } } } }
|
|
4989
|
+
*/
|
|
4990
|
+
const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
4991
|
+
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
4992
|
+
sandbox_permissions: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
4993
|
+
profiles: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({
|
|
4994
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
4995
|
+
tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.boolean())),
|
|
4996
|
+
enable_all_context_servers: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
4997
|
+
context_servers: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
4998
|
+
default_model: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
4999
|
+
})))
|
|
5000
|
+
});
|
|
5001
|
+
/**
|
|
4897
5002
|
* Permissions configuration.
|
|
4898
5003
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
4899
5004
|
* Values are pattern-to-action mappings for that tool category.
|
|
4900
5005
|
*
|
|
4901
5006
|
* The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
|
|
4902
5007
|
* `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
|
|
4903
|
-
* `antigravity-cli`/`augmentcode`/`kiro`/`codexcli` keys are tool-scoped
|
|
5008
|
+
* `antigravity-cli`/`augmentcode`/`kiro`/`codexcli`/`zed` keys are tool-scoped
|
|
4904
5009
|
* overrides consumed only by their respective translator (see the matching
|
|
4905
5010
|
* `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
|
|
4906
5011
|
* block and ignores them.
|
|
@@ -4940,14 +5045,15 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
4940
5045
|
augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
|
|
4941
5046
|
kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
|
|
4942
5047
|
codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
|
|
5048
|
+
zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
|
|
4943
5049
|
"antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4944
5050
|
copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
5051
|
+
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4945
5052
|
devin: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4946
5053
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4947
5054
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4948
5055
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
4949
|
-
rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
4950
|
-
zed: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
5056
|
+
rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
4951
5057
|
});
|
|
4952
5058
|
/**
|
|
4953
5059
|
* Full permissions file schema including optional $schema field.
|
|
@@ -5660,6 +5766,33 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
|
|
|
5660
5766
|
return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
|
|
5661
5767
|
}
|
|
5662
5768
|
//#endregion
|
|
5769
|
+
//#region src/constants/augmentcode-paths.ts
|
|
5770
|
+
const AUGMENTCODE_DIR = ".augment";
|
|
5771
|
+
const AUGMENTCODE_COMMANDS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "commands");
|
|
5772
|
+
/**
|
|
5773
|
+
* The cross-tool `.agents/commands/` root Auggie also discovers commands from.
|
|
5774
|
+
* Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
|
|
5775
|
+
* workspace and the home directory; rulesync writes to its own `.augment` root
|
|
5776
|
+
* and reads this one as well, mirroring how skills treat `.agents/skills`.
|
|
5777
|
+
* @see https://docs.augmentcode.com/cli/custom-commands
|
|
5778
|
+
*/
|
|
5779
|
+
const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = (0, node_path.join)(".agents", "commands");
|
|
5780
|
+
const AUGMENTCODE_SKILLS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "skills");
|
|
5781
|
+
const AUGMENTCODE_AGENTS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "agents");
|
|
5782
|
+
const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
|
|
5783
|
+
const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
5784
|
+
const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
5785
|
+
const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
5786
|
+
/**
|
|
5787
|
+
* Augment Code Review's custom guidelines file. Read from the repository root's
|
|
5788
|
+
* `.augment/` folder, project scope only — Augment documents no user-level
|
|
5789
|
+
* equivalent, and the reviewer runs against the committed tree.
|
|
5790
|
+
* @see https://docs.augmentcode.com/codereview/review-guidelines
|
|
5791
|
+
*/
|
|
5792
|
+
const AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME = "code_review_guidelines.yaml";
|
|
5793
|
+
const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
|
|
5794
|
+
const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
|
|
5795
|
+
//#endregion
|
|
5663
5796
|
//#region src/constants/cursor-paths.ts
|
|
5664
5797
|
const CURSOR_DIR = ".cursor";
|
|
5665
5798
|
const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
|
|
@@ -5709,6 +5842,19 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
|
|
|
5709
5842
|
*/
|
|
5710
5843
|
const TAKT_CONFIG_FILE_NAME = "config.yaml";
|
|
5711
5844
|
/**
|
|
5845
|
+
* Takt's runtime provider config (Takt 0.56.0+). Lives at `.takt/runtime.yaml`
|
|
5846
|
+
* (project) and `~/.takt/runtime.yaml` (global), and owns provider/model/
|
|
5847
|
+
* provider-option assignment. Takt generates the global file on first launch,
|
|
5848
|
+
* so most installs have one; "runtime mode" only activates when its `provider:`
|
|
5849
|
+
* section carries an actual assignment.
|
|
5850
|
+
*
|
|
5851
|
+
* rulesync only ever READS this file: it resolves the active provider from it,
|
|
5852
|
+
* and it refuses to write the legacy `provider_options` key into `config.yaml`
|
|
5853
|
+
* while runtime mode is active (Takt hard-fails on that combination).
|
|
5854
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
5855
|
+
*/
|
|
5856
|
+
const TAKT_RUNTIME_CONFIG_FILE_NAME = "runtime.yaml";
|
|
5857
|
+
/**
|
|
5712
5858
|
* Top-level key in Takt's `config.yaml` holding the workflow MCP transport
|
|
5713
5859
|
* allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
|
|
5714
5860
|
* transport must be explicitly enabled here before any workflow-defined MCP
|
|
@@ -6184,6 +6330,363 @@ function slugifyCheckName(value) {
|
|
|
6184
6330
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
|
|
6185
6331
|
}
|
|
6186
6332
|
//#endregion
|
|
6333
|
+
//#region src/features/checks/augmentcode-check.ts
|
|
6334
|
+
/** Augment's severity scale. Narrower than the canonical one. */
|
|
6335
|
+
const AUGMENTCODE_SEVERITIES = [
|
|
6336
|
+
"high",
|
|
6337
|
+
"medium",
|
|
6338
|
+
"low"
|
|
6339
|
+
];
|
|
6340
|
+
/**
|
|
6341
|
+
* Canonical severity to Augment's scale. Augment has no band above `high`, so
|
|
6342
|
+
* canonical `critical` folds into it — the alternative, dropping the rule or
|
|
6343
|
+
* demoting it to `medium`, would either lose the check or understate the one
|
|
6344
|
+
* severity a reviewer most wants raised. The fold is one-way: a `critical`
|
|
6345
|
+
* check generates `high` and imports back as `high`, so the canonical value is
|
|
6346
|
+
* not recoverable from Augment's file alone.
|
|
6347
|
+
*/
|
|
6348
|
+
const CANONICAL_TO_AUGMENTCODE_SEVERITY = {
|
|
6349
|
+
low: "low",
|
|
6350
|
+
medium: "medium",
|
|
6351
|
+
high: "high",
|
|
6352
|
+
critical: "high"
|
|
6353
|
+
};
|
|
6354
|
+
/**
|
|
6355
|
+
* Every field of an `areas.<key>` entry is required upstream, so a check with no
|
|
6356
|
+
* `severity` still has to emit one. `medium` is the neutral middle of Augment's
|
|
6357
|
+
* three bands: defaulting to `high` would inflate every unannotated check past
|
|
6358
|
+
* the ones deliberately marked `medium`, and `low` would bury them.
|
|
6359
|
+
*/
|
|
6360
|
+
const DEFAULT_AUGMENTCODE_SEVERITY = "medium";
|
|
6361
|
+
/**
|
|
6362
|
+
* Augment's own example uses `["**"]`, and `globs` is required per area. Built
|
|
6363
|
+
* fresh per area rather than shared: js-yaml serializes a repeated reference as
|
|
6364
|
+
* an anchor/alias pair, which would litter a file Augment tells users to edit by
|
|
6365
|
+
* hand with `&ref_0` / `*ref_0`.
|
|
6366
|
+
*/
|
|
6367
|
+
const defaultAreaGlobs = () => ["**"];
|
|
6368
|
+
/**
|
|
6369
|
+
* The `augmentcode` block of a check's frontmatter, carrying the two things the
|
|
6370
|
+
* canonical check model has no home for: which area a rule belongs to (so
|
|
6371
|
+
* several checks can share one), and the area's `globs`.
|
|
6372
|
+
*/
|
|
6373
|
+
const AugmentcodeCheckOverrideSchema = zod_mini.z.looseObject({
|
|
6374
|
+
/** Area key this check's rule is grouped under; defaults to the check's slug. */
|
|
6375
|
+
area: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.minLength(1))),
|
|
6376
|
+
/** Human-readable `areas.<key>.description`; defaults to the check's own. */
|
|
6377
|
+
areaDescription: zod_mini.z.optional(zod_mini.z.string()),
|
|
6378
|
+
globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string().check(zod_mini.z.minLength(1)))),
|
|
6379
|
+
/** Rule `id`; defaults to the check's slug. */
|
|
6380
|
+
id: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.minLength(1)))
|
|
6381
|
+
});
|
|
6382
|
+
function parseOverride$1(raw, filePath, logger) {
|
|
6383
|
+
if (raw === void 0) return {};
|
|
6384
|
+
if (!isPlainObject$1(raw)) {
|
|
6385
|
+
logger?.warn(`Ignoring the \`augmentcode\` block in ${filePath}: expected a mapping.`);
|
|
6386
|
+
return {};
|
|
6387
|
+
}
|
|
6388
|
+
const result = AugmentcodeCheckOverrideSchema.safeParse(raw);
|
|
6389
|
+
if (!result.success) throw new Error(`Invalid \`augmentcode\` block in ${filePath}: ${result.error.message}`, { cause: result.error });
|
|
6390
|
+
return result.data;
|
|
6391
|
+
}
|
|
6392
|
+
function stemOf$1(rulesyncCheck) {
|
|
6393
|
+
return (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
|
|
6394
|
+
}
|
|
6395
|
+
/**
|
|
6396
|
+
* Disambiguate a name against the ones already taken. Used for rule ids on
|
|
6397
|
+
* generate (two same-named checks in different subdirectories) and for check
|
|
6398
|
+
* file names on import (one rule id repeated across two areas), since a
|
|
6399
|
+
* collision in either direction silently loses one of the pair.
|
|
6400
|
+
*/
|
|
6401
|
+
function uniqueName(preferred, used) {
|
|
6402
|
+
let name = preferred;
|
|
6403
|
+
let suffix = 2;
|
|
6404
|
+
while (used.has(name)) {
|
|
6405
|
+
name = `${preferred}-${suffix}`;
|
|
6406
|
+
suffix += 1;
|
|
6407
|
+
}
|
|
6408
|
+
used.add(name);
|
|
6409
|
+
return name;
|
|
6410
|
+
}
|
|
6411
|
+
/**
|
|
6412
|
+
* A rule's `description` is the whole instruction Augment acts on, so the check
|
|
6413
|
+
* body is preferred; the frontmatter `description` is a summary, used only when
|
|
6414
|
+
* there is no body. Both empty falls back to the check name, which at least
|
|
6415
|
+
* names the concern rather than emitting an empty required field.
|
|
6416
|
+
*/
|
|
6417
|
+
function toRuleDescription(rulesyncCheck) {
|
|
6418
|
+
const body = rulesyncCheck.getBody().trim();
|
|
6419
|
+
if (body.length > 0) return body;
|
|
6420
|
+
return rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck);
|
|
6421
|
+
}
|
|
6422
|
+
function toRule(rulesyncCheck, override, usedRuleIds) {
|
|
6423
|
+
const severity = rulesyncCheck.getFrontmatter().severity;
|
|
6424
|
+
return {
|
|
6425
|
+
id: uniqueName(override.id ?? slugifyCheckName(stemOf$1(rulesyncCheck)), usedRuleIds),
|
|
6426
|
+
description: toRuleDescription(rulesyncCheck),
|
|
6427
|
+
severity: severity ? CANONICAL_TO_AUGMENTCODE_SEVERITY[severity] ?? DEFAULT_AUGMENTCODE_SEVERITY : DEFAULT_AUGMENTCODE_SEVERITY
|
|
6428
|
+
};
|
|
6429
|
+
}
|
|
6430
|
+
/**
|
|
6431
|
+
* An authored area key is used verbatim. Slugifying it would rewrite the
|
|
6432
|
+
* underscores in Augment's own documented example (`memory_safety`), and since
|
|
6433
|
+
* import writes the key back unchanged, the next generate would build a second
|
|
6434
|
+
* area under the slugified spelling while the original stayed put — the same
|
|
6435
|
+
* rules twice. Only the file-stem default is slugified, because that one has to
|
|
6436
|
+
* become a legal area key from an arbitrary file name.
|
|
6437
|
+
*/
|
|
6438
|
+
function areaKeyFor(rulesyncCheck, override) {
|
|
6439
|
+
return override.area ?? slugifyCheckName(stemOf$1(rulesyncCheck));
|
|
6440
|
+
}
|
|
6441
|
+
/** Rule ids belonging to areas this generate preserves rather than rewrites. */
|
|
6442
|
+
function collectRuleIds(existingAreas, claimedKeys) {
|
|
6443
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6444
|
+
for (const [areaKey, rawArea] of Object.entries(existingAreas)) {
|
|
6445
|
+
if (claimedKeys.has(areaKey) || !isPlainObject$1(rawArea)) continue;
|
|
6446
|
+
for (const rawRule of readArea(rawArea).rules) {
|
|
6447
|
+
const rule = readRule(rawRule);
|
|
6448
|
+
if (rule) ids.add(rule.id);
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
return ids;
|
|
6452
|
+
}
|
|
6453
|
+
/**
|
|
6454
|
+
* Group the checks into `areas`. One area per check by default; checks naming
|
|
6455
|
+
* the same `augmentcode.area` share one, with the first check to name it
|
|
6456
|
+
* supplying the area's `description` and `globs`.
|
|
6457
|
+
*/
|
|
6458
|
+
function buildAreas({ entries, reservedRuleIds }) {
|
|
6459
|
+
const areas = /* @__PURE__ */ new Map();
|
|
6460
|
+
const usedRuleIds = new Set(reservedRuleIds);
|
|
6461
|
+
for (const { rulesyncCheck, override } of entries) {
|
|
6462
|
+
const key = areaKeyFor(rulesyncCheck, override);
|
|
6463
|
+
const rule = toRule(rulesyncCheck, override, usedRuleIds);
|
|
6464
|
+
const existing = areas.get(key);
|
|
6465
|
+
if (existing) {
|
|
6466
|
+
existing.rules.push(rule);
|
|
6467
|
+
continue;
|
|
6468
|
+
}
|
|
6469
|
+
areas.set(key, {
|
|
6470
|
+
description: override.areaDescription || rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck),
|
|
6471
|
+
globs: override.globs ?? defaultAreaGlobs(),
|
|
6472
|
+
rules: [rule]
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6475
|
+
return Object.fromEntries(areas);
|
|
6476
|
+
}
|
|
6477
|
+
function parseGuidelines(fileContent, filePath) {
|
|
6478
|
+
if (fileContent.trim().length === 0) return {};
|
|
6479
|
+
let parsed;
|
|
6480
|
+
try {
|
|
6481
|
+
parsed = loadYaml(fileContent);
|
|
6482
|
+
} catch (error) {
|
|
6483
|
+
throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
6484
|
+
}
|
|
6485
|
+
if (parsed === void 0 || parsed === null) return {};
|
|
6486
|
+
if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: expected a mapping at the document root.`);
|
|
6487
|
+
return parsed;
|
|
6488
|
+
}
|
|
6489
|
+
/** An area's shared fields, read defensively out of hand-written YAML. */
|
|
6490
|
+
function readArea(rawArea) {
|
|
6491
|
+
const rawGlobs = Array.isArray(rawArea.globs) ? rawArea.globs : void 0;
|
|
6492
|
+
const globs = rawGlobs?.filter((glob) => typeof glob === "string");
|
|
6493
|
+
const authoredGlobs = globs && (globs.length > 0 || rawGlobs?.length === 0) ? globs : void 0;
|
|
6494
|
+
return {
|
|
6495
|
+
...typeof rawArea.description === "string" && { description: rawArea.description },
|
|
6496
|
+
...authoredGlobs && { globs: authoredGlobs },
|
|
6497
|
+
rules: Array.isArray(rawArea.rules) ? rawArea.rules : []
|
|
6498
|
+
};
|
|
6499
|
+
}
|
|
6500
|
+
/**
|
|
6501
|
+
* A rule missing `id` or `description` is not a shape Augment itself reads, so
|
|
6502
|
+
* it is skipped rather than imported as a check with an invented field.
|
|
6503
|
+
*/
|
|
6504
|
+
function readRule(rawRule) {
|
|
6505
|
+
if (!isPlainObject$1(rawRule)) return void 0;
|
|
6506
|
+
const id = typeof rawRule.id === "string" ? rawRule.id : void 0;
|
|
6507
|
+
const description = typeof rawRule.description === "string" ? rawRule.description : void 0;
|
|
6508
|
+
if (!id || !description) return void 0;
|
|
6509
|
+
const severity = AUGMENTCODE_SEVERITIES.find((value) => value === rawRule.severity);
|
|
6510
|
+
return {
|
|
6511
|
+
id,
|
|
6512
|
+
description,
|
|
6513
|
+
...severity && { severity }
|
|
6514
|
+
};
|
|
6515
|
+
}
|
|
6516
|
+
/**
|
|
6517
|
+
* Checks adapter for Augment Code Review's custom guidelines
|
|
6518
|
+
* (`.augment/code_review_guidelines.yaml`).
|
|
6519
|
+
*
|
|
6520
|
+
* Augment groups review rules into named **areas**: each area carries a
|
|
6521
|
+
* `description`, the `globs` it applies to, and a list of `rules`, every rule
|
|
6522
|
+
* being an `id` / `description` / `severity` triple. Every one of those fields
|
|
6523
|
+
* is required upstream. Rulesync maps one `.rulesync/checks/*.md` onto one rule,
|
|
6524
|
+
* in an area of its own unless an `augmentcode.area` groups several together —
|
|
6525
|
+
* so the whole set collapses into this one file via {@link fromRulesyncChecks}
|
|
6526
|
+
* rather than a file per check.
|
|
6527
|
+
*
|
|
6528
|
+
* **Severity is lossy in one direction.** Augment's scale is `high` / `medium` /
|
|
6529
|
+
* `low` with no band above `high`, so canonical `critical` maps to `high` and
|
|
6530
|
+
* imports back as `high`. A check with no `severity` at all emits `medium`,
|
|
6531
|
+
* since the field cannot be omitted.
|
|
6532
|
+
*
|
|
6533
|
+
* **`file_paths_to_ignore`** is recognized and preserved verbatim, but never
|
|
6534
|
+
* authored or imported: the canonical check model has no ignore surface, and
|
|
6535
|
+
* inventing one here is a separate design question.
|
|
6536
|
+
*
|
|
6537
|
+
* **Generation merges rather than replaces.** Augment's own documentation tells
|
|
6538
|
+
* users to hand-write this file, so only the areas the current check set claims
|
|
6539
|
+
* are rewritten; every other area, `file_paths_to_ignore`, and any unknown
|
|
6540
|
+
* top-level key survive. The cost of that is that rulesync cannot tell its own
|
|
6541
|
+
* leftovers from a hand-written area: renaming a check strands the area under
|
|
6542
|
+
* the old key, and dropping the last Augment-targeting check leaves the areas in
|
|
6543
|
+
* place with a warning rather than guessing which of them to delete. Deleting
|
|
6544
|
+
* the file outright is likewise refused whenever it exists.
|
|
6545
|
+
*
|
|
6546
|
+
* Project scope only — the reviewer reads the file from the committed
|
|
6547
|
+
* repository, and Augment documents no user-level equivalent.
|
|
6548
|
+
*
|
|
6549
|
+
* @see https://docs.augmentcode.com/codereview/review-guidelines
|
|
6550
|
+
*/
|
|
6551
|
+
var AugmentcodeCheck = class AugmentcodeCheck extends ToolCheck {
|
|
6552
|
+
static getSettablePaths(_options = {}) {
|
|
6553
|
+
return {
|
|
6554
|
+
relativeDirPath: AUGMENTCODE_DIR,
|
|
6555
|
+
relativeFilePath: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME
|
|
6556
|
+
};
|
|
6557
|
+
}
|
|
6558
|
+
static isTargetedByRulesyncCheck(rulesyncCheck) {
|
|
6559
|
+
return this.isTargetedByRulesyncCheckDefault({
|
|
6560
|
+
rulesyncCheck,
|
|
6561
|
+
toolTarget: "augmentcode"
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
/**
|
|
6565
|
+
* Ownership guard the processor consults before it deletes anything for this
|
|
6566
|
+
* tool. Unlike the Markdown adapters, whose section markers say which text is
|
|
6567
|
+
* rulesync's, YAML carries no such marker — js-yaml drops comments on rewrite,
|
|
6568
|
+
* and an unknown top-level key risks Augment's own parser. So an existing file
|
|
6569
|
+
* is never rulesync's to delete: it may hold hand-written areas, and there is
|
|
6570
|
+
* no way to prove otherwise.
|
|
6571
|
+
*/
|
|
6572
|
+
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
6573
|
+
const paths = AugmentcodeCheck.getSettablePaths();
|
|
6574
|
+
return !await fileExists((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "code_review_guidelines.yaml"));
|
|
6575
|
+
}
|
|
6576
|
+
static fromRulesyncCheck(_params) {
|
|
6577
|
+
throw new Error("AugmentCode checks are built from all checks at once; use fromRulesyncChecks.");
|
|
6578
|
+
}
|
|
6579
|
+
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
6580
|
+
const paths = AugmentcodeCheck.getSettablePaths({ global });
|
|
6581
|
+
const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
|
|
6582
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
6583
|
+
const existing = parseGuidelines(await readFileContentOrNull(filePath) ?? "", filePath);
|
|
6584
|
+
const existingAreas = isPlainObject$1(existing.areas) ? existing.areas : {};
|
|
6585
|
+
if (rulesyncChecks.length === 0) {
|
|
6586
|
+
if (Object.keys(existingAreas).length > 0) logger?.warn(`AugmentCode checks: no check targets AugmentCode, but ${filePath} still holds review areas. They are left in place — rulesync cannot tell the ones it generated from ones you wrote, so removing them is a manual edit.`);
|
|
6587
|
+
return [];
|
|
6588
|
+
}
|
|
6589
|
+
const entries = rulesyncChecks.map((rulesyncCheck) => ({
|
|
6590
|
+
rulesyncCheck,
|
|
6591
|
+
override: parseOverride$1(rulesyncCheck.getFrontmatter().augmentcode, (0, node_path.join)(RULESYNC_CHECKS_RELATIVE_DIR_PATH, rulesyncCheck.getRelativeFilePath()), logger)
|
|
6592
|
+
}));
|
|
6593
|
+
const generatedAreas = buildAreas({
|
|
6594
|
+
entries,
|
|
6595
|
+
reservedRuleIds: collectRuleIds(existingAreas, new Set(entries.map(({ rulesyncCheck, override }) => areaKeyFor(rulesyncCheck, override))))
|
|
6596
|
+
});
|
|
6597
|
+
const fileContent = (0, js_yaml.dump)({
|
|
6598
|
+
...existing,
|
|
6599
|
+
areas: {
|
|
6600
|
+
...existingAreas,
|
|
6601
|
+
...generatedAreas
|
|
6602
|
+
}
|
|
6603
|
+
}, {
|
|
6604
|
+
lineWidth: -1,
|
|
6605
|
+
noRefs: true
|
|
6606
|
+
});
|
|
6607
|
+
return [new AugmentcodeCheck({
|
|
6608
|
+
outputRoot,
|
|
6609
|
+
relativeDirPath: paths.relativeDirPath,
|
|
6610
|
+
relativeFilePath,
|
|
6611
|
+
fileContent,
|
|
6612
|
+
global
|
|
6613
|
+
})];
|
|
6614
|
+
}
|
|
6615
|
+
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
6616
|
+
const paths = AugmentcodeCheck.getSettablePaths({ global });
|
|
6617
|
+
const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
|
|
6618
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
6619
|
+
return new AugmentcodeCheck({
|
|
6620
|
+
outputRoot,
|
|
6621
|
+
relativeDirPath: paths.relativeDirPath,
|
|
6622
|
+
relativeFilePath,
|
|
6623
|
+
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
6624
|
+
global
|
|
6625
|
+
});
|
|
6626
|
+
}
|
|
6627
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
6628
|
+
return new AugmentcodeCheck({
|
|
6629
|
+
outputRoot,
|
|
6630
|
+
relativeDirPath,
|
|
6631
|
+
relativeFilePath,
|
|
6632
|
+
fileContent: "",
|
|
6633
|
+
validate: false,
|
|
6634
|
+
global
|
|
6635
|
+
});
|
|
6636
|
+
}
|
|
6637
|
+
validate() {
|
|
6638
|
+
return {
|
|
6639
|
+
success: true,
|
|
6640
|
+
error: null
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
toRulesyncCheck() {
|
|
6644
|
+
const first = this.toRulesyncChecks()[0];
|
|
6645
|
+
if (!first) throw new Error(`No review areas found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
6646
|
+
return first;
|
|
6647
|
+
}
|
|
6648
|
+
/**
|
|
6649
|
+
* One check per rule, not per area: an area with three rules is three
|
|
6650
|
+
* independent review instructions, and collapsing them into one check would
|
|
6651
|
+
* make the next generate emit a single merged rule in their place. The area's
|
|
6652
|
+
* key, description and globs ride along in each check's `augmentcode` block,
|
|
6653
|
+
* so regenerating regroups the rules exactly where they were.
|
|
6654
|
+
*/
|
|
6655
|
+
toRulesyncChecks() {
|
|
6656
|
+
const filePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
6657
|
+
const guidelines = parseGuidelines(this.getFileContent(), filePath);
|
|
6658
|
+
const areas = isPlainObject$1(guidelines.areas) ? guidelines.areas : {};
|
|
6659
|
+
const checks = [];
|
|
6660
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
6661
|
+
for (const [areaKey, rawArea] of Object.entries(areas)) {
|
|
6662
|
+
if (!isPlainObject$1(rawArea)) continue;
|
|
6663
|
+
const { description: areaDescription, globs, rules } = readArea(rawArea);
|
|
6664
|
+
for (const rawRule of rules) {
|
|
6665
|
+
const rule = readRule(rawRule);
|
|
6666
|
+
if (!rule) continue;
|
|
6667
|
+
const name = uniqueName(slugifyCheckName(rule.id) || slugifyCheckName(areaKey) || "check", usedNames);
|
|
6668
|
+
checks.push(new RulesyncCheck({
|
|
6669
|
+
outputRoot: ".",
|
|
6670
|
+
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
6671
|
+
relativeFilePath: `${name}.md`,
|
|
6672
|
+
frontmatter: {
|
|
6673
|
+
targets: ["*"],
|
|
6674
|
+
...rule.severity && { severity: rule.severity },
|
|
6675
|
+
augmentcode: {
|
|
6676
|
+
area: areaKey,
|
|
6677
|
+
...areaDescription !== void 0 && { areaDescription },
|
|
6678
|
+
...globs && { globs },
|
|
6679
|
+
id: rule.id
|
|
6680
|
+
}
|
|
6681
|
+
},
|
|
6682
|
+
body: rule.description
|
|
6683
|
+
}));
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
return checks;
|
|
6687
|
+
}
|
|
6688
|
+
};
|
|
6689
|
+
//#endregion
|
|
6187
6690
|
//#region src/features/checks/aggregated-check-file.ts
|
|
6188
6691
|
/**
|
|
6189
6692
|
* Shared machinery for the tools whose checks surface is **one aggregated
|
|
@@ -7106,6 +7609,22 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7106
7609
|
}
|
|
7107
7610
|
}
|
|
7108
7611
|
},
|
|
7612
|
+
".github/copilot/settings.json": {
|
|
7613
|
+
format: "json",
|
|
7614
|
+
invalidRootPolicy: "error",
|
|
7615
|
+
features: { permissions: {
|
|
7616
|
+
kind: "replace-owned-keys",
|
|
7617
|
+
ownedKeys: ["deniedUrls"]
|
|
7618
|
+
} }
|
|
7619
|
+
},
|
|
7620
|
+
".copilot/settings.json": {
|
|
7621
|
+
format: "json",
|
|
7622
|
+
invalidRootPolicy: "error",
|
|
7623
|
+
features: { permissions: {
|
|
7624
|
+
kind: "replace-owned-keys",
|
|
7625
|
+
ownedKeys: ["allowedUrls", "deniedUrls"]
|
|
7626
|
+
} }
|
|
7627
|
+
},
|
|
7109
7628
|
".vscode/settings.json": {
|
|
7110
7629
|
format: "jsonc",
|
|
7111
7630
|
invalidRootPolicy: "error",
|
|
@@ -7186,6 +7705,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7186
7705
|
}
|
|
7187
7706
|
}
|
|
7188
7707
|
},
|
|
7708
|
+
".config/muse/settings.json": {
|
|
7709
|
+
format: "json",
|
|
7710
|
+
invalidRootPolicy: "error",
|
|
7711
|
+
features: { mcp: {
|
|
7712
|
+
kind: "replace-owned-keys",
|
|
7713
|
+
ownedKeys: ["mcp_servers", "schema_version"]
|
|
7714
|
+
} }
|
|
7715
|
+
},
|
|
7189
7716
|
".kiro/agents/default.json": {
|
|
7190
7717
|
format: "json",
|
|
7191
7718
|
features: {
|
|
@@ -7833,6 +8360,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
|
|
|
7833
8360
|
filePattern: "*.md"
|
|
7834
8361
|
}
|
|
7835
8362
|
}],
|
|
8363
|
+
["augmentcode", {
|
|
8364
|
+
class: AugmentcodeCheck,
|
|
8365
|
+
meta: {
|
|
8366
|
+
supportsGlobal: false,
|
|
8367
|
+
filePattern: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME,
|
|
8368
|
+
committedOutput: true
|
|
8369
|
+
}
|
|
8370
|
+
}],
|
|
7836
8371
|
["cursor", {
|
|
7837
8372
|
class: CursorCheck,
|
|
7838
8373
|
meta: {
|
|
@@ -8620,26 +9155,6 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
|
|
|
8620
9155
|
}
|
|
8621
9156
|
};
|
|
8622
9157
|
//#endregion
|
|
8623
|
-
//#region src/constants/augmentcode-paths.ts
|
|
8624
|
-
const AUGMENTCODE_DIR = ".augment";
|
|
8625
|
-
const AUGMENTCODE_COMMANDS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "commands");
|
|
8626
|
-
/**
|
|
8627
|
-
* The cross-tool `.agents/commands/` root Auggie also discovers commands from.
|
|
8628
|
-
* Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
|
|
8629
|
-
* workspace and the home directory; rulesync writes to its own `.augment` root
|
|
8630
|
-
* and reads this one as well, mirroring how skills treat `.agents/skills`.
|
|
8631
|
-
* @see https://docs.augmentcode.com/cli/custom-commands
|
|
8632
|
-
*/
|
|
8633
|
-
const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = (0, node_path.join)(".agents", "commands");
|
|
8634
|
-
const AUGMENTCODE_SKILLS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "skills");
|
|
8635
|
-
const AUGMENTCODE_AGENTS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "agents");
|
|
8636
|
-
const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
|
|
8637
|
-
const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
8638
|
-
const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
8639
|
-
const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
8640
|
-
const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
|
|
8641
|
-
const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
|
|
8642
|
-
//#endregion
|
|
8643
9158
|
//#region src/features/commands/augmentcode-command.ts
|
|
8644
9159
|
const AugmentcodeCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
8645
9160
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -9173,6 +9688,8 @@ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
|
|
|
9173
9688
|
const COPILOT_MCP_DIR = ".vscode";
|
|
9174
9689
|
const COPILOT_MCP_FILE_NAME = "mcp.json";
|
|
9175
9690
|
const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
9691
|
+
const COPILOTCLI_SETTINGS_FILE_NAME = "settings.json";
|
|
9692
|
+
const COPILOTCLI_PROJECT_SETTINGS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "copilot");
|
|
9176
9693
|
const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
|
|
9177
9694
|
const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
|
|
9178
9695
|
const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
|
|
@@ -9432,6 +9949,7 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
|
9432
9949
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
9433
9950
|
const DEVIN_IGNORE_FILE_NAME = ".devinignore";
|
|
9434
9951
|
const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
|
|
9952
|
+
const DEVIN_WINDSURF_IGNORE_FILE_NAME = ".windsurfignore";
|
|
9435
9953
|
const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
|
|
9436
9954
|
const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
|
|
9437
9955
|
//#endregion
|
|
@@ -9786,7 +10304,7 @@ var GooseCommandConfigFile = class extends ToolFile {
|
|
|
9786
10304
|
* The whole file is a YAML mapping (not frontmatter + markdown body), so the
|
|
9787
10305
|
* class stores the parsed recipe object rather than a frontmatter/body split.
|
|
9788
10306
|
*
|
|
9789
|
-
* @see https://
|
|
10307
|
+
* @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
|
|
9790
10308
|
*/
|
|
9791
10309
|
const GooseCommandRecipeSchema = zod_mini.z.looseObject({
|
|
9792
10310
|
version: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -13280,7 +13798,7 @@ function collectHandlers({ effectiveHooks, eventMap }) {
|
|
|
13280
13798
|
}
|
|
13281
13799
|
return handlerGroups;
|
|
13282
13800
|
}
|
|
13283
|
-
function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
|
|
13801
|
+
function buildCommandLines$1({ handler, usesToolName, blocksToolCall }) {
|
|
13284
13802
|
const lines = [];
|
|
13285
13803
|
const indent = usesToolName && handler.matcher ? " " : " ";
|
|
13286
13804
|
if (usesToolName && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral$1(handler.matcher)}).test(event.tool)) {`);
|
|
@@ -13305,7 +13823,7 @@ function buildSubscriptionLines$1(handlerGroups) {
|
|
|
13305
13823
|
const usesToolName = AMP_TOOL_EVENTS.has(ampEvent) && handlers.some((handler) => handler.matcher);
|
|
13306
13824
|
const blocksToolCall = ampEvent === "tool.call";
|
|
13307
13825
|
lines.push(` amp.on(${JSON.stringify(ampEvent)}, async (${usesToolName ? "event, ctx" : "_event, ctx"}) => {`);
|
|
13308
|
-
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
13826
|
+
for (const handler of handlers) lines.push(...buildCommandLines$1({
|
|
13309
13827
|
handler,
|
|
13310
13828
|
usesToolName,
|
|
13311
13829
|
blocksToolCall
|
|
@@ -15290,8 +15808,10 @@ const CopilotHookEntrySchema = zod_mini.z.looseObject({
|
|
|
15290
15808
|
bash: zod_mini.z.optional(zod_mini.z.string()),
|
|
15291
15809
|
powershell: zod_mini.z.optional(zod_mini.z.string()),
|
|
15292
15810
|
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
15811
|
+
cwd: zod_mini.z.optional(zod_mini.z.string()),
|
|
15293
15812
|
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
15294
|
-
timeoutSec: zod_mini.z.optional(zod_mini.z.number())
|
|
15813
|
+
timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
|
|
15814
|
+
timeout: zod_mini.z.optional(zod_mini.z.number())
|
|
15295
15815
|
});
|
|
15296
15816
|
/**
|
|
15297
15817
|
* Convert canonical hooks config to Copilot format.
|
|
@@ -15371,6 +15891,22 @@ function resolveImportCommand$1(entry, logger) {
|
|
|
15371
15891
|
return typeof entry.command === "string" ? { command: entry.command } : {};
|
|
15372
15892
|
}
|
|
15373
15893
|
/**
|
|
15894
|
+
* Extract the non-command fields preserved across import.
|
|
15895
|
+
*
|
|
15896
|
+
* Generate re-emits any non-canonical key verbatim through `rest`, so a key
|
|
15897
|
+
* dropped here does not survive an import → generate round trip. `cwd` is a
|
|
15898
|
+
* documented Copilot hook field and was previously lost that way.
|
|
15899
|
+
*
|
|
15900
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
15901
|
+
* @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
|
|
15902
|
+
*/
|
|
15903
|
+
function importPassthrough$1(entry) {
|
|
15904
|
+
const passthrough = {};
|
|
15905
|
+
if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
|
|
15906
|
+
if (entry.env !== void 0) passthrough.env = entry.env;
|
|
15907
|
+
return passthrough;
|
|
15908
|
+
}
|
|
15909
|
+
/**
|
|
15374
15910
|
* Extract hooks from Copilot hooks JSON into canonical format.
|
|
15375
15911
|
* Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
|
|
15376
15912
|
*/
|
|
@@ -15386,13 +15922,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
15386
15922
|
if (!parseResult.success) continue;
|
|
15387
15923
|
const entry = parseResult.data;
|
|
15388
15924
|
const { command, shell } = resolveImportCommand$1(entry, logger);
|
|
15389
|
-
const timeout = entry.timeoutSec;
|
|
15925
|
+
const timeout = entry.timeoutSec ?? entry.timeout;
|
|
15390
15926
|
defs.push({
|
|
15391
15927
|
type: "command",
|
|
15392
15928
|
...command !== void 0 && { command },
|
|
15393
15929
|
...shell !== void 0 && { shell },
|
|
15394
|
-
...
|
|
15395
|
-
...
|
|
15930
|
+
...timeout !== void 0 && { timeout },
|
|
15931
|
+
...importPassthrough$1(entry)
|
|
15396
15932
|
});
|
|
15397
15933
|
}
|
|
15398
15934
|
if (defs.length > 0) canonical[eventName] = defs;
|
|
@@ -15675,19 +16211,19 @@ function importPassthrough(entry) {
|
|
|
15675
16211
|
* A shell-specific field carries its `shell` through so re-export writes the
|
|
15676
16212
|
* same field back. An entry using only the portable `command` field leaves
|
|
15677
16213
|
* `shell` unset, which re-export renders as the portable field again.
|
|
16214
|
+
*
|
|
16215
|
+
* When both shell-specific fields are present, `bash` wins and a warning names
|
|
16216
|
+
* the ignored `powershell`. The choice is deliberately not platform-dependent:
|
|
16217
|
+
* importing on Windows must not produce a different canonical config than
|
|
16218
|
+
* importing the same file on Linux, which would make the rulesync hooks file
|
|
16219
|
+
* differ per machine for anyone who checks it in.
|
|
15678
16220
|
*/
|
|
15679
16221
|
function resolveImportCommand(entry, logger) {
|
|
15680
16222
|
const hasBash = typeof entry.bash === "string";
|
|
15681
16223
|
const hasPowershell = typeof entry.powershell === "string";
|
|
15682
16224
|
if (hasBash && hasPowershell) {
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
const ignored = isWindows ? "bash" : "powershell";
|
|
15686
|
-
logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
|
|
15687
|
-
return isWindows ? {
|
|
15688
|
-
command: entry.powershell,
|
|
15689
|
-
shell: "powershell"
|
|
15690
|
-
} : {
|
|
16225
|
+
logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
|
|
16226
|
+
return {
|
|
15691
16227
|
command: entry.bash,
|
|
15692
16228
|
shell: "bash"
|
|
15693
16229
|
};
|
|
@@ -16346,7 +16882,7 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
16346
16882
|
*
|
|
16347
16883
|
* The JSON shape matches Claude Code's: each PascalCase event maps to an array of
|
|
16348
16884
|
* `{ matcher, hooks: [{ type: "command", command }] }` entries.
|
|
16349
|
-
* @see https://
|
|
16885
|
+
* @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
|
|
16350
16886
|
*/
|
|
16351
16887
|
var GooseHooks = class GooseHooks extends ToolHooks {
|
|
16352
16888
|
constructor(params) {
|
|
@@ -16547,6 +17083,13 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
|
|
|
16547
17083
|
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
16548
17084
|
*/
|
|
16549
17085
|
const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "post_tool_call"]);
|
|
17086
|
+
/**
|
|
17087
|
+
* The only Hermes event that can block, and therefore the only one whose
|
|
17088
|
+
* entries accept `fail_closed`. Upstream warns and ignores the key on every
|
|
17089
|
+
* other event ("only blocking-capable events can fail closed").
|
|
17090
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
17091
|
+
*/
|
|
17092
|
+
const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
|
|
16550
17093
|
const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
|
|
16551
17094
|
const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
|
|
16552
17095
|
/**
|
|
@@ -16580,7 +17123,9 @@ function isHermesHookEventEntry(key, value) {
|
|
|
16580
17123
|
* unsupported hook types centrally). `matcher` is only carried through for
|
|
16581
17124
|
* `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
|
|
16582
17125
|
* warning, mirroring how other adapters (e.g. AugmentCode) handle
|
|
16583
|
-
* matcher-less lifecycle events.
|
|
17126
|
+
* matcher-less lifecycle events. The canonical `failClosed` field (shared with
|
|
17127
|
+
* Cursor, whose semantics Hermes copied) becomes `fail_closed`, which upstream
|
|
17128
|
+
* only honours on `pre_tool_call`.
|
|
16584
17129
|
*/
|
|
16585
17130
|
function definitionsToHermesEntries({ event, sourceEvent = event, definitions, logger }) {
|
|
16586
17131
|
const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(event);
|
|
@@ -16591,6 +17136,10 @@ function definitionsToHermesEntries({ event, sourceEvent = event, definitions, l
|
|
|
16591
17136
|
if (typeof definition.matcher === "string" && definition.matcher !== "") if (supportsMatcher) entry.matcher = definition.matcher;
|
|
16592
17137
|
else logger?.warn(`matcher "${definition.matcher}" on "${sourceEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
|
|
16593
17138
|
if (typeof definition.timeout === "number") entry.timeout = definition.timeout;
|
|
17139
|
+
if (typeof definition.failClosed === "boolean") {
|
|
17140
|
+
if (event === HERMESAGENT_FAIL_CLOSED_EVENT) entry.fail_closed = definition.failClosed;
|
|
17141
|
+
else if (definition.failClosed) logger?.warn(`failClosed on "${sourceEvent}" hook will be ignored — Hermes Agent only supports fail_closed on pre_tool_call`);
|
|
17142
|
+
}
|
|
16594
17143
|
entries.push(entry);
|
|
16595
17144
|
}
|
|
16596
17145
|
return entries;
|
|
@@ -16642,6 +17191,37 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
16642
17191
|
return result;
|
|
16643
17192
|
}
|
|
16644
17193
|
/**
|
|
17194
|
+
* Reads a hook entry's fail-closed flag. Upstream accepts its own `fail_closed`
|
|
17195
|
+
* and the Cursor/Claude Code `failClosed` spelling alike, so both have to be
|
|
17196
|
+
* read back into the single canonical field.
|
|
17197
|
+
*/
|
|
17198
|
+
function readHermesFailClosed(entry) {
|
|
17199
|
+
if (typeof entry.fail_closed === "boolean") return entry.fail_closed;
|
|
17200
|
+
if (typeof entry.failClosed === "boolean") return entry.failClosed;
|
|
17201
|
+
}
|
|
17202
|
+
/**
|
|
17203
|
+
* Converts one serialized Hermes hook entry back into a canonical definition,
|
|
17204
|
+
* or `undefined` when it is not a hook rulesync models (no string `command`).
|
|
17205
|
+
* `matcher` and `fail_closed` are read only on the events upstream honours them
|
|
17206
|
+
* on, so an imported value is never one the next generate warns about and drops.
|
|
17207
|
+
*/
|
|
17208
|
+
function hermesEntryToDefinition({ nativeEvent, raw }) {
|
|
17209
|
+
if (!isRecord$1(raw)) return;
|
|
17210
|
+
const entry = raw;
|
|
17211
|
+
if (typeof entry.command !== "string") return;
|
|
17212
|
+
const def = {
|
|
17213
|
+
type: "command",
|
|
17214
|
+
command: entry.command
|
|
17215
|
+
};
|
|
17216
|
+
if (HERMESAGENT_MATCHER_EVENTS.has(nativeEvent) && typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
|
|
17217
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout;
|
|
17218
|
+
if (nativeEvent === HERMESAGENT_FAIL_CLOSED_EVENT) {
|
|
17219
|
+
const failClosed = readHermesFailClosed(entry);
|
|
17220
|
+
if (failClosed !== void 0) def.failClosed = failClosed;
|
|
17221
|
+
}
|
|
17222
|
+
return def;
|
|
17223
|
+
}
|
|
17224
|
+
/**
|
|
16645
17225
|
* Reverse {@link canonicalToHermesHooks}: parse Hermes's native
|
|
16646
17226
|
* `hooks: { <event>: [...] }` map back into a canonical event → definition[]
|
|
16647
17227
|
* record. Native events with no canonical equivalent (`pre_verify`,
|
|
@@ -16655,19 +17235,10 @@ function hermesHooksToCanonical(hooks) {
|
|
|
16655
17235
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
16656
17236
|
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
16657
17237
|
const rulesyncEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent] ?? nativeEvent;
|
|
16658
|
-
const defs =
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
if (typeof entry.command !== "string") continue;
|
|
16663
|
-
const def = {
|
|
16664
|
-
type: "command",
|
|
16665
|
-
command: entry.command
|
|
16666
|
-
};
|
|
16667
|
-
if (HERMESAGENT_MATCHER_EVENTS.has(nativeEvent) && typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
|
|
16668
|
-
if (typeof entry.timeout === "number") def.timeout = entry.timeout;
|
|
16669
|
-
defs.push(def);
|
|
16670
|
-
}
|
|
17238
|
+
const defs = entries.map((raw) => hermesEntryToDefinition({
|
|
17239
|
+
nativeEvent,
|
|
17240
|
+
raw
|
|
17241
|
+
})).filter((def) => def !== void 0);
|
|
16671
17242
|
if (defs.length > 0) canonical[rulesyncEvent] = defs;
|
|
16672
17243
|
}
|
|
16673
17244
|
return canonical;
|
|
@@ -17255,6 +17826,38 @@ function stripTrustedDirectoryWrapper(command) {
|
|
|
17255
17826
|
if (posix?.[1]) return posix[1];
|
|
17256
17827
|
return command.match(/^set "RULESYNC_KIMI_HOOK_CWD=1" && cd \/d "(?:""|[^"])*" && ([\s\S]*)$/)?.[1] ?? command;
|
|
17257
17828
|
}
|
|
17829
|
+
/**
|
|
17830
|
+
* Native Kimi Code events whose Event Reference row lists the matcher as
|
|
17831
|
+
* "Empty string". Kimi Code documents `matcher` as "a regular expression to
|
|
17832
|
+
* filter event targets; if omitted, matches all", so on these events the
|
|
17833
|
+
* regex is tested against `""`: any non-trivial matcher simply never matches
|
|
17834
|
+
* and the hook silently never runs. Dropping the matcher is what makes the
|
|
17835
|
+
* hook fire at all, which is the authored intent — these events have no target
|
|
17836
|
+
* to filter on in the first place.
|
|
17837
|
+
*
|
|
17838
|
+
* Keyed on native names because the check runs after the canonical → native
|
|
17839
|
+
* mapping: `SessionHeartbeat` and `Interrupt` have no canonical counterpart and
|
|
17840
|
+
* are only reachable through a per-tool `kimi-code` override naming them
|
|
17841
|
+
* directly.
|
|
17842
|
+
*
|
|
17843
|
+
* Deliberately narrower than Claude Code's equivalent set: Kimi Code's
|
|
17844
|
+
* `UserPromptSubmit` matches the submitted prompt text, and `PermissionResult`
|
|
17845
|
+
* matches the tool name, so a matcher on either is meaningful and is kept.
|
|
17846
|
+
*
|
|
17847
|
+
* @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
|
|
17848
|
+
*/
|
|
17849
|
+
const KIMI_CODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
|
|
17850
|
+
"Stop",
|
|
17851
|
+
"SessionHeartbeat",
|
|
17852
|
+
"Interrupt"
|
|
17853
|
+
]);
|
|
17854
|
+
/** Resolve the `matcher` part of an emitted entry, dropping dead matchers. */
|
|
17855
|
+
function resolveMatcherPart({ matcher, nativeEvent, logger }) {
|
|
17856
|
+
if (!matcher) return {};
|
|
17857
|
+
if (!KIMI_CODE_NO_MATCHER_EVENTS.has(nativeEvent)) return { matcher };
|
|
17858
|
+
logger?.warn(`matcher "${matcher}" on "${nativeEvent}" hook will be ignored — this event does not support matchers`);
|
|
17859
|
+
return {};
|
|
17860
|
+
}
|
|
17258
17861
|
function buildEffectiveHooks(config, toolOverrideHooks) {
|
|
17259
17862
|
const supported = new Set(KIMI_CODE_HOOK_EVENTS);
|
|
17260
17863
|
const shared = {};
|
|
@@ -17284,7 +17887,11 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
|
|
|
17284
17887
|
command: definition.command,
|
|
17285
17888
|
trustedDirectory
|
|
17286
17889
|
}),
|
|
17287
|
-
...
|
|
17890
|
+
...resolveMatcherPart({
|
|
17891
|
+
matcher: definition.matcher,
|
|
17892
|
+
nativeEvent,
|
|
17893
|
+
logger
|
|
17894
|
+
}),
|
|
17288
17895
|
...validTimeout && timeout !== void 0 && { timeout }
|
|
17289
17896
|
});
|
|
17290
17897
|
}
|
|
@@ -17482,17 +18089,33 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
|
17482
18089
|
}
|
|
17483
18090
|
return entries;
|
|
17484
18091
|
}
|
|
17485
|
-
|
|
18092
|
+
/**
|
|
18093
|
+
* The single `HooksConfig` key every Kiro target reads its tool-specific
|
|
18094
|
+
* overrides from.
|
|
18095
|
+
*
|
|
18096
|
+
* `kiro-ide` and `kiro-cli` write the same `.kiro/hooks/rulesync.json` at both
|
|
18097
|
+
* scopes, so per-target override blocks would make that one file's content
|
|
18098
|
+
* depend on generation order (last writer wins). All Kiro variants therefore
|
|
18099
|
+
* share the `kiro` block — the same resolution the Kiro MCP and permissions
|
|
18100
|
+
* wiring already use for the file they share.
|
|
18101
|
+
*/
|
|
18102
|
+
const KIRO_HOOKS_OVERRIDE_KEY = "kiro";
|
|
18103
|
+
/**
|
|
18104
|
+
* Override keys a user might reach for that nothing reads, mapped to the key
|
|
18105
|
+
* that is actually read.
|
|
18106
|
+
*/
|
|
18107
|
+
const KIRO_HOOKS_IGNORED_OVERRIDE_KEYS = ["kiro-ide", "kiro-cli"];
|
|
18108
|
+
function canonicalToKiroIdeHooks(config) {
|
|
17486
18109
|
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
17487
18110
|
const sharedHooks = {};
|
|
17488
18111
|
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
17489
18112
|
const effectiveHooks = {
|
|
17490
18113
|
...sharedHooks,
|
|
17491
|
-
...config[
|
|
18114
|
+
...config[KIRO_HOOKS_OVERRIDE_KEY]?.hooks
|
|
17492
18115
|
};
|
|
17493
18116
|
const entries = [];
|
|
17494
18117
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
17495
|
-
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
18118
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES[eventName] ?? eventName;
|
|
17496
18119
|
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
17497
18120
|
}
|
|
17498
18121
|
return entries;
|
|
@@ -17537,6 +18160,11 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
17537
18160
|
* still writes the embedded `.kiro/agents/default.json` agent-config shape,
|
|
17538
18161
|
* which Kiro CLI 3.0 no longer reads.
|
|
17539
18162
|
*
|
|
18163
|
+
* Because both targets write the very same file, they resolve their
|
|
18164
|
+
* tool-specific overrides from one shared block
|
|
18165
|
+
* ({@link KIRO_HOOKS_OVERRIDE_KEY}) rather than per-target blocks, so
|
|
18166
|
+
* generating either or both targets always yields the same file.
|
|
18167
|
+
*
|
|
17540
18168
|
* @see https://kiro.dev/docs/hooks/
|
|
17541
18169
|
*/
|
|
17542
18170
|
var KiroIdeHooks = class extends ToolHooks {
|
|
@@ -17549,14 +18177,6 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17549
18177
|
}, null, 2)
|
|
17550
18178
|
});
|
|
17551
18179
|
}
|
|
17552
|
-
/**
|
|
17553
|
-
* The `HooksConfig` key whose `hooks` block provides tool-specific overrides
|
|
17554
|
-
* for this target. {@link import("./kiro-cli-hooks.js").KiroCliHooks}
|
|
17555
|
-
* overrides this to `kiro-cli`.
|
|
17556
|
-
*/
|
|
17557
|
-
static getOverrideKey() {
|
|
17558
|
-
return "kiro-ide";
|
|
17559
|
-
}
|
|
17560
18180
|
static getSettablePaths(_options = {}) {
|
|
17561
18181
|
return {
|
|
17562
18182
|
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
@@ -17577,9 +18197,14 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17577
18197
|
validate
|
|
17578
18198
|
});
|
|
17579
18199
|
}
|
|
17580
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
18200
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
17581
18201
|
const paths = this.getSettablePaths({ global });
|
|
17582
|
-
const
|
|
18202
|
+
const config = rulesyncHooks.getJson();
|
|
18203
|
+
for (const ignoredKey of KIRO_HOOKS_IGNORED_OVERRIDE_KEYS) {
|
|
18204
|
+
if (config[ignoredKey]?.hooks === void 0) continue;
|
|
18205
|
+
logger?.warn(`The "${ignoredKey}.hooks" block in ${(0, node_path.join)(rulesyncHooks.getRelativeDirPath(), rulesyncHooks.getRelativeFilePath())} is ignored. Author it under the "${KIRO_HOOKS_OVERRIDE_KEY}.hooks" key instead: the Kiro IDE and Kiro CLI targets write the same hooks file, so they read one shared block.`);
|
|
18206
|
+
}
|
|
18207
|
+
const hooks = canonicalToKiroIdeHooks(config);
|
|
17583
18208
|
const fileContent = JSON.stringify({
|
|
17584
18209
|
version: "v1",
|
|
17585
18210
|
hooks
|
|
@@ -17600,10 +18225,9 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17600
18225
|
throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17601
18226
|
}
|
|
17602
18227
|
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
17603
|
-
const overrideKey = this.constructor.getOverrideKey();
|
|
17604
18228
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
17605
18229
|
hooks,
|
|
17606
|
-
overrideKey
|
|
18230
|
+
overrideKey: KIRO_HOOKS_OVERRIDE_KEY
|
|
17607
18231
|
}), null, 2) });
|
|
17608
18232
|
}
|
|
17609
18233
|
validate() {
|
|
@@ -17631,9 +18255,13 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17631
18255
|
* Hooks generator for the **Kiro CLI**.
|
|
17632
18256
|
*
|
|
17633
18257
|
* Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
|
|
17634
|
-
* Kiro IDE reads
|
|
17635
|
-
*
|
|
17636
|
-
*
|
|
18258
|
+
* Kiro IDE reads — the same directory, and in rulesync the same
|
|
18259
|
+
* `.kiro/hooks/rulesync.json` file — so this is {@link KiroIdeHooks} under a
|
|
18260
|
+
* second target name, down to the shared `kiro` override block both resolve
|
|
18261
|
+
* their tool-specific hooks from. A per-target override block would make that
|
|
18262
|
+
* one file's content depend on which target was generated last, which is
|
|
18263
|
+
* exactly what the shared block avoids (the Kiro MCP and permissions wiring
|
|
18264
|
+
* resolve their shared files the same way).
|
|
17637
18265
|
*
|
|
17638
18266
|
* The embedded `.kiro/agents/default.json` agent-hook format this target used
|
|
17639
18267
|
* to emit is documented as not working in 3.0, so it is left to the deprecated
|
|
@@ -17642,11 +18270,7 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17642
18270
|
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
17643
18271
|
* @see https://kiro.dev/docs/hooks/
|
|
17644
18272
|
*/
|
|
17645
|
-
var KiroCliHooks = class extends KiroIdeHooks {
|
|
17646
|
-
static getOverrideKey() {
|
|
17647
|
-
return "kiro-cli";
|
|
17648
|
-
}
|
|
17649
|
-
};
|
|
18273
|
+
var KiroCliHooks = class extends KiroIdeHooks {};
|
|
17650
18274
|
//#endregion
|
|
17651
18275
|
//#region src/features/hooks/kiro-hooks.ts
|
|
17652
18276
|
/**
|
|
@@ -17671,14 +18295,30 @@ function buildKiroEntriesForEvent(definitions) {
|
|
|
17671
18295
|
}
|
|
17672
18296
|
return entries;
|
|
17673
18297
|
}
|
|
17674
|
-
|
|
18298
|
+
/**
|
|
18299
|
+
* Event keys the embedded agent-config format defines: the canonical events it
|
|
18300
|
+
* supports plus its own native spellings (`agentSpawn`, `userPromptSubmit`, …).
|
|
18301
|
+
*
|
|
18302
|
+
* The `kiro` override block is shared with the standalone `.kiro/hooks/*.json`
|
|
18303
|
+
* targets, whose vocabulary is different (`PostFileSave`, `PreTaskExec`, …).
|
|
18304
|
+
* Passing those through here would write event keys Kiro does not define into
|
|
18305
|
+
* `.kiro/agents/default.json`, so they are dropped instead.
|
|
18306
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
18307
|
+
*/
|
|
18308
|
+
const KIRO_AGENT_CONFIG_EVENT_KEYS = /* @__PURE__ */ new Set([...KIRO_HOOK_EVENTS, ...KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES]);
|
|
18309
|
+
function canonicalToKiroHooks({ config, logger }) {
|
|
17675
18310
|
const overrideKey = "kiro";
|
|
17676
18311
|
const kiroSupported = new Set(KIRO_HOOK_EVENTS);
|
|
17677
18312
|
const sharedHooks = {};
|
|
17678
18313
|
for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
|
|
18314
|
+
const overrideHooks = {};
|
|
18315
|
+
const droppedEvents = [];
|
|
18316
|
+
for (const [event, defs] of Object.entries(config[overrideKey]?.hooks ?? {})) if (KIRO_AGENT_CONFIG_EVENT_KEYS.has(event)) overrideHooks[event] = defs;
|
|
18317
|
+
else droppedEvents.push(event);
|
|
18318
|
+
if (droppedEvents.length > 0) logger?.warn(`Skipped hook event(s) from the "kiro" override block for the deprecated kiro agent config (no event key of that format): ${droppedEvents.join(", ")}. They are emitted for the kiro-cli / kiro-ide targets, which read the same block.`);
|
|
17679
18319
|
const effectiveHooks = {
|
|
17680
18320
|
...sharedHooks,
|
|
17681
|
-
...
|
|
18321
|
+
...overrideHooks
|
|
17682
18322
|
};
|
|
17683
18323
|
const kiro = {};
|
|
17684
18324
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
@@ -17773,11 +18413,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
17773
18413
|
validate
|
|
17774
18414
|
});
|
|
17775
18415
|
}
|
|
17776
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
18416
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
17777
18417
|
const paths = KiroHooks.getSettablePaths({ global });
|
|
17778
18418
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
17779
18419
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
17780
|
-
const kiroHooks = canonicalToKiroHooks(
|
|
18420
|
+
const kiroHooks = canonicalToKiroHooks({
|
|
18421
|
+
config: rulesyncHooks.getJson(),
|
|
18422
|
+
logger
|
|
18423
|
+
});
|
|
17781
18424
|
const fileContent = applySharedConfigPatch({
|
|
17782
18425
|
fileKey: sharedConfigFileKey(paths),
|
|
17783
18426
|
feature: "hooks",
|
|
@@ -17893,6 +18536,30 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
|
|
|
17893
18536
|
*/
|
|
17894
18537
|
const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
|
|
17895
18538
|
/**
|
|
18539
|
+
* `tool_call` is the only Pi extension event that can block, and it is Pi's
|
|
18540
|
+
* only tool gate. Its return contract is
|
|
18541
|
+
* `{ block: true, reason?: string, terminate?: boolean }`.
|
|
18542
|
+
*
|
|
18543
|
+
* @see https://github.com/earendil-works/pi/blob/v0.84.1/packages/coding-agent/docs/extensions.md#tool_call
|
|
18544
|
+
*/
|
|
18545
|
+
const PI_BLOCKING_EVENT = "tool_call";
|
|
18546
|
+
/**
|
|
18547
|
+
* Helper emitted alongside blocking handlers. `promisify(exec)` rejects on a
|
|
18548
|
+
* non-zero exit with an error carrying `stdout` / `stderr` / `code`, so the
|
|
18549
|
+
* reason is derived from the rejection rather than from a resolved exit code.
|
|
18550
|
+
*/
|
|
18551
|
+
const BLOCK_REASON_HELPER_LINES = [
|
|
18552
|
+
"function toBlockReason(error: unknown): string {",
|
|
18553
|
+
" const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
|
|
18554
|
+
" const stderr = String(result?.stderr ?? \"\").trim();",
|
|
18555
|
+
" if (stderr) return stderr;",
|
|
18556
|
+
" const stdout = String(result?.stdout ?? \"\").trim();",
|
|
18557
|
+
" if (stdout) return stdout;",
|
|
18558
|
+
" if (result?.code !== undefined) return `Hook command failed with exit code ${result.code}.`;",
|
|
18559
|
+
" return error instanceof Error ? error.message : String(error);",
|
|
18560
|
+
"}"
|
|
18561
|
+
];
|
|
18562
|
+
/**
|
|
17896
18563
|
* Validate a hook matcher as a regular expression and return it as a JS
|
|
17897
18564
|
* string-literal (JSON.stringify quoting) safe to embed in generated code.
|
|
17898
18565
|
*/
|
|
@@ -17929,6 +18596,22 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
|
|
|
17929
18596
|
}
|
|
17930
18597
|
return handlerGroups;
|
|
17931
18598
|
}
|
|
18599
|
+
function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
|
|
18600
|
+
const lines = [];
|
|
18601
|
+
const gated = usesToolName && Boolean(handler.matcher);
|
|
18602
|
+
const indent = gated ? " " : " ";
|
|
18603
|
+
const embeddedCommand = JSON.stringify(handler.command);
|
|
18604
|
+
if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
|
|
18605
|
+
if (blocksToolCall) {
|
|
18606
|
+
lines.push(`${indent}try {`);
|
|
18607
|
+
lines.push(`${indent} await run(${embeddedCommand});`);
|
|
18608
|
+
lines.push(`${indent}} catch (error) {`);
|
|
18609
|
+
lines.push(`${indent} return { block: true, reason: toBlockReason(error) };`);
|
|
18610
|
+
lines.push(`${indent}}`);
|
|
18611
|
+
} else lines.push(`${indent}await run(${embeddedCommand});`);
|
|
18612
|
+
if (gated) lines.push(" }");
|
|
18613
|
+
return lines;
|
|
18614
|
+
}
|
|
17932
18615
|
function buildSubscriptionLines(handlerGroups) {
|
|
17933
18616
|
const lines = [];
|
|
17934
18617
|
for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
|
|
@@ -17937,14 +18620,11 @@ function buildSubscriptionLines(handlerGroups) {
|
|
|
17937
18620
|
const usesEvent = usesToolName || gatesOnAssistant;
|
|
17938
18621
|
lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
|
|
17939
18622
|
if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
|
|
17940
|
-
for (const handler of handlers) {
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
lines.push(" }");
|
|
17946
|
-
} else lines.push(` await run(${embeddedCommand});`);
|
|
17947
|
-
}
|
|
18623
|
+
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
18624
|
+
handler,
|
|
18625
|
+
usesToolName,
|
|
18626
|
+
blocksToolCall: piEvent === PI_BLOCKING_EVENT
|
|
18627
|
+
}));
|
|
17948
18628
|
lines.push(" });");
|
|
17949
18629
|
}
|
|
17950
18630
|
return lines;
|
|
@@ -17953,8 +18633,9 @@ function buildSubscriptionLines(handlerGroups) {
|
|
|
17953
18633
|
* Generate the rulesync-owned Pi extension (a TypeScript module with a
|
|
17954
18634
|
* default-export factory receiving Pi's ExtensionAPI) that subscribes to the
|
|
17955
18635
|
* mapped lifecycle events and executes the configured hook commands via the
|
|
17956
|
-
* platform shell.
|
|
17957
|
-
*
|
|
18636
|
+
* platform shell. Handlers observe events, except on `tool_call` — Pi's only
|
|
18637
|
+
* blocking event and its only tool gate — where a hook command that exits
|
|
18638
|
+
* non-zero denies the call with `{ block: true, reason }`.
|
|
17958
18639
|
*
|
|
17959
18640
|
* @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
|
|
17960
18641
|
*/
|
|
@@ -17966,10 +18647,12 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
|
|
|
17966
18647
|
};
|
|
17967
18648
|
const effectiveHooks = {};
|
|
17968
18649
|
for (const [event, defs] of Object.entries(configHooks)) if (supported.has(event)) effectiveHooks[event] = defs;
|
|
17969
|
-
const
|
|
18650
|
+
const handlerGroups = collectPiHandlers({
|
|
17970
18651
|
effectiveHooks,
|
|
17971
18652
|
eventMap
|
|
17972
|
-
})
|
|
18653
|
+
});
|
|
18654
|
+
const subscriptionLines = buildSubscriptionLines(handlerGroups);
|
|
18655
|
+
const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]);
|
|
17973
18656
|
const lines = ["// Generated by rulesync. Do not edit manually."];
|
|
17974
18657
|
if (subscriptionLines.length === 0) {
|
|
17975
18658
|
lines.push("export default function () {}");
|
|
@@ -17983,6 +18666,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
|
|
|
17983
18666
|
lines.push("");
|
|
17984
18667
|
lines.push("const run = promisify(exec);");
|
|
17985
18668
|
lines.push("");
|
|
18669
|
+
if (needsBlockReasonHelper) {
|
|
18670
|
+
lines.push(...BLOCK_REASON_HELPER_LINES);
|
|
18671
|
+
lines.push("");
|
|
18672
|
+
}
|
|
17986
18673
|
lines.push("export default function (pi: ExtensionAPI) {");
|
|
17987
18674
|
lines.push(...subscriptionLines);
|
|
17988
18675
|
lines.push("}");
|
|
@@ -18712,6 +19399,20 @@ function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
|
|
|
18712
19399
|
}
|
|
18713
19400
|
return [...eventsWithMatcher];
|
|
18714
19401
|
}
|
|
19402
|
+
/**
|
|
19403
|
+
* Targets that read their tool-scoped `{key}.hooks` override from a differently
|
|
19404
|
+
* named key. The two Kiro standalone-format targets write the very same
|
|
19405
|
+
* `.kiro/hooks/rulesync.json`, so they share the `kiro` block: a per-target
|
|
19406
|
+
* block would make that one file's content depend on generation order. The same
|
|
19407
|
+
* resolution is used by the MCP (`MCP_BLOCK_KEY_ALIASES`) and permissions
|
|
19408
|
+
* (`PERMISSION_OVERRIDE_KEY_ALIASES`) features for the files they share.
|
|
19409
|
+
*/
|
|
19410
|
+
const HOOKS_OVERRIDE_KEY_ALIASES = {
|
|
19411
|
+
"kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
|
|
19412
|
+
"kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
|
|
19413
|
+
};
|
|
19414
|
+
/** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
|
|
19415
|
+
const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
|
|
18715
19416
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
18716
19417
|
["amp", {
|
|
18717
19418
|
class: AmpHooks,
|
|
@@ -18952,7 +19653,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
18952
19653
|
},
|
|
18953
19654
|
supportedEvents: KIRO_HOOK_EVENTS,
|
|
18954
19655
|
supportedHookTypes: ["command"],
|
|
18955
|
-
supportsMatcher: true
|
|
19656
|
+
supportsMatcher: true,
|
|
19657
|
+
passthroughOverrideEvents: true
|
|
18956
19658
|
}],
|
|
18957
19659
|
["kiro-cli", {
|
|
18958
19660
|
class: KiroCliHooks,
|
|
@@ -19132,7 +19834,7 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
19132
19834
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
19133
19835
|
const config = rulesyncHooks.getJson();
|
|
19134
19836
|
const sharedHooks = config.hooks;
|
|
19135
|
-
const overrideHooks = config[this.toolTarget]?.hooks ?? {};
|
|
19837
|
+
const overrideHooks = config[HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget]?.hooks ?? {};
|
|
19136
19838
|
const effectiveHooks = {
|
|
19137
19839
|
...sharedHooks,
|
|
19138
19840
|
...overrideHooks
|
|
@@ -19158,14 +19860,14 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
19158
19860
|
}
|
|
19159
19861
|
for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
|
|
19160
19862
|
}
|
|
19161
|
-
if (this.toolTarget
|
|
19863
|
+
if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
|
|
19162
19864
|
const skippedEvents = new Set(unsupportedEventNames({
|
|
19163
19865
|
factory,
|
|
19164
19866
|
sharedHooks,
|
|
19165
19867
|
effectiveHooks
|
|
19166
19868
|
}));
|
|
19167
19869
|
const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
|
|
19168
|
-
if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only kiro-ide supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
|
|
19870
|
+
if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide standalone hooks format supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
|
|
19169
19871
|
}
|
|
19170
19872
|
const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
|
|
19171
19873
|
factory,
|
|
@@ -19664,9 +20366,11 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
|
|
|
19664
20366
|
*
|
|
19665
20367
|
* Generates the brand-aligned `.devinignore` file with gitignore-compatible
|
|
19666
20368
|
* syntax. Devin automatically respects `.gitignore` patterns and has built-in
|
|
19667
|
-
* defaults for node_modules/ and hidden files. On import, the
|
|
19668
|
-
* `.codeiumignore`
|
|
19669
|
-
* round-trip.
|
|
20369
|
+
* defaults for node_modules/ and hidden files. On import, the pre-rebrand
|
|
20370
|
+
* `.codeiumignore` and `.windsurfignore` filenames are read as fallbacks so
|
|
20371
|
+
* existing projects still round-trip. The docs list the three names side by
|
|
20372
|
+
* side without defining a precedence between them, so the order below is
|
|
20373
|
+
* rulesync's own choice: brand-aligned name first, then the legacy names.
|
|
19670
20374
|
*
|
|
19671
20375
|
* In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
|
|
19672
20376
|
* instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
|
|
@@ -19705,9 +20409,16 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
19705
20409
|
validate,
|
|
19706
20410
|
global
|
|
19707
20411
|
});
|
|
19708
|
-
const
|
|
19709
|
-
|
|
19710
|
-
|
|
20412
|
+
const candidateFileNames = [
|
|
20413
|
+
relativeFilePath,
|
|
20414
|
+
DEVIN_LEGACY_IGNORE_FILE_NAME,
|
|
20415
|
+
DEVIN_WINDSURF_IGNORE_FILE_NAME
|
|
20416
|
+
];
|
|
20417
|
+
let resolvedFilePath = relativeFilePath;
|
|
20418
|
+
for (const candidateFileName of candidateFileNames) if (await fileExists((0, node_path.join)(outputRoot, relativeDirPath, candidateFileName))) {
|
|
20419
|
+
resolvedFilePath = candidateFileName;
|
|
20420
|
+
break;
|
|
20421
|
+
}
|
|
19711
20422
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, resolvedFilePath));
|
|
19712
20423
|
return new DevinIgnore({
|
|
19713
20424
|
outputRoot,
|
|
@@ -22936,8 +23647,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
|
22936
23647
|
* shape and cannot express `url`/`headers`, so remote servers are skipped with
|
|
22937
23648
|
* a warning in project mode (use `--global` to sync them instead).
|
|
22938
23649
|
*
|
|
22939
|
-
* @see https://
|
|
22940
|
-
* @see https://github.com/
|
|
23650
|
+
* @see https://goose-docs.ai/docs/getting-started/using-extensions/
|
|
23651
|
+
* @see https://github.com/aaif-goose/goose/pull/9471
|
|
22941
23652
|
*/
|
|
22942
23653
|
var GooseMcp = class GooseMcp extends ToolMcp {
|
|
22943
23654
|
config;
|
|
@@ -23208,7 +23919,7 @@ function resolveHermesTimeout(config) {
|
|
|
23208
23919
|
* alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
|
|
23209
23920
|
* path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
|
|
23210
23921
|
* `connect_timeout` (seconds), `supports_parallel_tool_calls`,
|
|
23211
|
-
* `keepalive_interval`, and `
|
|
23922
|
+
* `keepalive_interval`, `elicitation`, `trust`, and `identity_header` — verbatim
|
|
23212
23923
|
* from `source` to `target`. Field names are identical on both sides (the
|
|
23213
23924
|
* canonical `McpServerSchema` is a `looseObject`), so this serves export and
|
|
23214
23925
|
* import alike. See the Hermes mcp-config-reference.
|
|
@@ -23277,6 +23988,14 @@ function copyHermesAdvancedFields(source, target) {
|
|
|
23277
23988
|
target.elicitation = omitPrototypePollutionKeys(structuredClone(source.elicitation));
|
|
23278
23989
|
copied = true;
|
|
23279
23990
|
}
|
|
23991
|
+
if (typeof source.trust === "string") {
|
|
23992
|
+
target.trust = source.trust;
|
|
23993
|
+
copied = true;
|
|
23994
|
+
}
|
|
23995
|
+
if (isPlainObject$1(source.identity_header)) {
|
|
23996
|
+
target.identity_header = omitPrototypePollutionKeys(structuredClone(source.identity_header));
|
|
23997
|
+
copied = true;
|
|
23998
|
+
}
|
|
23280
23999
|
return copied;
|
|
23281
24000
|
}
|
|
23282
24001
|
/**
|
|
@@ -24441,6 +25160,213 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
24441
25160
|
}
|
|
24442
25161
|
};
|
|
24443
25162
|
//#endregion
|
|
25163
|
+
//#region src/constants/musecode-paths.ts
|
|
25164
|
+
const MUSECODE_RULE_FILE_NAME = "AGENTS.md";
|
|
25165
|
+
const MUSECODE_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
25166
|
+
const MUSECODE_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "muse");
|
|
25167
|
+
const MUSECODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(MUSECODE_GLOBAL_CONFIG_DIR_PATH, "skills");
|
|
25168
|
+
const MUSECODE_SETTINGS_FILE_NAME = "settings.json";
|
|
25169
|
+
//#endregion
|
|
25170
|
+
//#region src/features/mcp/musecode-mcp.ts
|
|
25171
|
+
const MUSECODE_GLOBAL_ONLY_MESSAGE = "Muse Code MCP is global-only; use --global to sync ~/.config/muse/settings.json";
|
|
25172
|
+
/**
|
|
25173
|
+
* Single spelling of the settings.json codec/policy, matching the
|
|
25174
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for `.config/muse/settings.json`:
|
|
25175
|
+
* fail closed on an unparseable root rather than replacing the user's primary
|
|
25176
|
+
* Muse Code config with generated output.
|
|
25177
|
+
*/
|
|
25178
|
+
function parseMusecodeSettings(fileContent, filePath) {
|
|
25179
|
+
return parseSharedConfig({
|
|
25180
|
+
format: "json",
|
|
25181
|
+
fileContent,
|
|
25182
|
+
filePath,
|
|
25183
|
+
invalidRootPolicy: "error"
|
|
25184
|
+
});
|
|
25185
|
+
}
|
|
25186
|
+
/**
|
|
25187
|
+
* Convert canonical rulesync servers to Muse Code's native `mcp_servers` shape.
|
|
25188
|
+
* Each entry carries a `transport` discriminator: `stdio` servers spawn a
|
|
25189
|
+
* `command` (single string) with `args`/`env`, and `streamable_http` servers
|
|
25190
|
+
* are reached at a `url` with optional `headers`. Only documented fields are
|
|
25191
|
+
* emitted; a canonical `disabled: true` maps to Muse's `enabled: false`.
|
|
25192
|
+
*/
|
|
25193
|
+
function convertToMusecodeFormat(mcpServers, logger) {
|
|
25194
|
+
const result = {};
|
|
25195
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
25196
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
25197
|
+
if (!isRecord$1(config)) continue;
|
|
25198
|
+
if (declaresNoTransport(config)) {
|
|
25199
|
+
warnAndSkipMcpServer({
|
|
25200
|
+
toolName: "Muse Code",
|
|
25201
|
+
serverName: name,
|
|
25202
|
+
reason: "no transport",
|
|
25203
|
+
logger
|
|
25204
|
+
});
|
|
25205
|
+
continue;
|
|
25206
|
+
}
|
|
25207
|
+
const converted = {};
|
|
25208
|
+
if (isRemoteMcpServer(config)) {
|
|
25209
|
+
const url = resolveRemoteMcpUrl(config);
|
|
25210
|
+
if (!url) {
|
|
25211
|
+
warnAndSkipMcpServer({
|
|
25212
|
+
toolName: "Muse Code",
|
|
25213
|
+
serverName: name,
|
|
25214
|
+
reason: "a remote transport without a url",
|
|
25215
|
+
logger
|
|
25216
|
+
});
|
|
25217
|
+
continue;
|
|
25218
|
+
}
|
|
25219
|
+
const stated = config.type ?? config.transport;
|
|
25220
|
+
if (stated === "sse" || stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) {
|
|
25221
|
+
warnAndSkipMcpServer({
|
|
25222
|
+
toolName: "Muse Code",
|
|
25223
|
+
serverName: name,
|
|
25224
|
+
reason: `the "${stated ?? "ws"}" transport, which Muse Code does not implement (only stdio and streamable_http are supported)`,
|
|
25225
|
+
logger
|
|
25226
|
+
});
|
|
25227
|
+
continue;
|
|
25228
|
+
}
|
|
25229
|
+
converted.transport = "streamable_http";
|
|
25230
|
+
converted.url = url;
|
|
25231
|
+
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
25232
|
+
} else {
|
|
25233
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
25234
|
+
if (!command) {
|
|
25235
|
+
warnAndSkipMcpServer({
|
|
25236
|
+
toolName: "Muse Code",
|
|
25237
|
+
serverName: name,
|
|
25238
|
+
reason: "a stdio transport without a command",
|
|
25239
|
+
logger
|
|
25240
|
+
});
|
|
25241
|
+
continue;
|
|
25242
|
+
}
|
|
25243
|
+
converted.transport = "stdio";
|
|
25244
|
+
converted.command = command;
|
|
25245
|
+
converted.args = args;
|
|
25246
|
+
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
25247
|
+
}
|
|
25248
|
+
if (config.disabled === true) converted.enabled = false;
|
|
25249
|
+
result[name] = converted;
|
|
25250
|
+
}
|
|
25251
|
+
return result;
|
|
25252
|
+
}
|
|
25253
|
+
/**
|
|
25254
|
+
* Convert Muse Code's native `mcp_servers` shape back to canonical rulesync
|
|
25255
|
+
* servers. The `transport` discriminator is dropped (`streamable_http` is not a
|
|
25256
|
+
* canonical enum value; the transport is re-derived from `command`/`url` on the
|
|
25257
|
+
* next generate), `enabled: false` maps back to `disabled: true`, and unknown
|
|
25258
|
+
* keys (e.g. `mode`, `framing`) pass through untouched.
|
|
25259
|
+
*/
|
|
25260
|
+
function convertFromMusecodeFormat(musecodeMcp) {
|
|
25261
|
+
const result = {};
|
|
25262
|
+
for (const [name, config] of Object.entries(musecodeMcp)) {
|
|
25263
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
25264
|
+
const converted = {};
|
|
25265
|
+
for (const [key, value] of Object.entries(config)) {
|
|
25266
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
25267
|
+
if (key === "transport") continue;
|
|
25268
|
+
if (key === "enabled") {
|
|
25269
|
+
if (value === false) converted.disabled = true;
|
|
25270
|
+
continue;
|
|
25271
|
+
}
|
|
25272
|
+
converted[key] = value;
|
|
25273
|
+
}
|
|
25274
|
+
result[name] = converted;
|
|
25275
|
+
}
|
|
25276
|
+
return result;
|
|
25277
|
+
}
|
|
25278
|
+
/**
|
|
25279
|
+
* Meta Muse Code MCP servers.
|
|
25280
|
+
*
|
|
25281
|
+
* Muse Code reads MCP servers only from the `mcp_servers` block of the GLOBAL
|
|
25282
|
+
* user settings file `~/.config/muse/settings.json`; no project-scoped MCP
|
|
25283
|
+
* location is documented. The settings file must carry
|
|
25284
|
+
* `"schema_version": 1` — a file that omits that key fails every command at
|
|
25285
|
+
* startup with `malformed settings file` — so the key is bootstrapped when the
|
|
25286
|
+
* file is created and preserved when it already exists. Other settings keys are
|
|
25287
|
+
* preserved via the shared-config gateway, and the file is never deleted.
|
|
25288
|
+
*
|
|
25289
|
+
* @see https://dev.meta.ai/docs/muse-code/configuration.md
|
|
25290
|
+
* @see https://dev.meta.ai/docs/muse-code/extending.md
|
|
25291
|
+
*/
|
|
25292
|
+
var MusecodeMcp = class MusecodeMcp extends ToolMcp {
|
|
25293
|
+
json;
|
|
25294
|
+
constructor(params) {
|
|
25295
|
+
super(params);
|
|
25296
|
+
this.json = parseMusecodeSettings(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
25297
|
+
}
|
|
25298
|
+
getJson() {
|
|
25299
|
+
return this.json;
|
|
25300
|
+
}
|
|
25301
|
+
isDeletable() {
|
|
25302
|
+
return false;
|
|
25303
|
+
}
|
|
25304
|
+
static getSettablePaths(_options) {
|
|
25305
|
+
return {
|
|
25306
|
+
relativeDirPath: MUSECODE_GLOBAL_CONFIG_DIR_PATH,
|
|
25307
|
+
relativeFilePath: MUSECODE_SETTINGS_FILE_NAME
|
|
25308
|
+
};
|
|
25309
|
+
}
|
|
25310
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
25311
|
+
if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
|
|
25312
|
+
const paths = this.getSettablePaths({ global });
|
|
25313
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
25314
|
+
return new MusecodeMcp({
|
|
25315
|
+
outputRoot,
|
|
25316
|
+
relativeDirPath: paths.relativeDirPath,
|
|
25317
|
+
relativeFilePath: paths.relativeFilePath,
|
|
25318
|
+
fileContent,
|
|
25319
|
+
validate,
|
|
25320
|
+
global
|
|
25321
|
+
});
|
|
25322
|
+
}
|
|
25323
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
25324
|
+
if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
|
|
25325
|
+
const paths = this.getSettablePaths({ global });
|
|
25326
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
25327
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
25328
|
+
const existing = parseMusecodeSettings(existingContent, filePath);
|
|
25329
|
+
const converted = convertToMusecodeFormat(rulesyncMcp.getMcpServers(), logger);
|
|
25330
|
+
return new MusecodeMcp({
|
|
25331
|
+
outputRoot,
|
|
25332
|
+
relativeDirPath: paths.relativeDirPath,
|
|
25333
|
+
relativeFilePath: paths.relativeFilePath,
|
|
25334
|
+
fileContent: applySharedConfigPatch({
|
|
25335
|
+
fileKey: sharedConfigFileKey(paths),
|
|
25336
|
+
feature: "mcp",
|
|
25337
|
+
existingContent,
|
|
25338
|
+
patch: {
|
|
25339
|
+
mcp_servers: converted,
|
|
25340
|
+
...existing.schema_version === void 0 && { schema_version: 1 }
|
|
25341
|
+
},
|
|
25342
|
+
filePath
|
|
25343
|
+
}),
|
|
25344
|
+
validate,
|
|
25345
|
+
global
|
|
25346
|
+
});
|
|
25347
|
+
}
|
|
25348
|
+
toRulesyncMcp() {
|
|
25349
|
+
const converted = convertFromMusecodeFormat(isRecord$1(this.json.mcp_servers) ? this.json.mcp_servers : {});
|
|
25350
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
25351
|
+
}
|
|
25352
|
+
validate() {
|
|
25353
|
+
return {
|
|
25354
|
+
success: true,
|
|
25355
|
+
error: null
|
|
25356
|
+
};
|
|
25357
|
+
}
|
|
25358
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
25359
|
+
return new MusecodeMcp({
|
|
25360
|
+
outputRoot,
|
|
25361
|
+
relativeDirPath,
|
|
25362
|
+
relativeFilePath,
|
|
25363
|
+
fileContent: JSON.stringify({ mcp_servers: {} }, null, 2),
|
|
25364
|
+
validate: false,
|
|
25365
|
+
global
|
|
25366
|
+
});
|
|
25367
|
+
}
|
|
25368
|
+
};
|
|
25369
|
+
//#endregion
|
|
24444
25370
|
//#region src/features/mcp/opencode-mcp.ts
|
|
24445
25371
|
const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
|
|
24446
25372
|
const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
|
|
@@ -26397,6 +27323,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
26397
27323
|
supportsDisabledTools: false
|
|
26398
27324
|
}
|
|
26399
27325
|
}],
|
|
27326
|
+
["musecode", {
|
|
27327
|
+
class: MusecodeMcp,
|
|
27328
|
+
meta: {
|
|
27329
|
+
supportsProject: false,
|
|
27330
|
+
supportsGlobal: true,
|
|
27331
|
+
supportsEnabledTools: false,
|
|
27332
|
+
supportsDisabledTools: false
|
|
27333
|
+
}
|
|
27334
|
+
}],
|
|
26400
27335
|
["opencode", {
|
|
26401
27336
|
class: OpencodeMcp,
|
|
26402
27337
|
meta: {
|
|
@@ -29212,6 +30147,163 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
29212
30147
|
}
|
|
29213
30148
|
};
|
|
29214
30149
|
//#endregion
|
|
30150
|
+
//#region src/features/permissions/copilotcli-permissions.ts
|
|
30151
|
+
/**
|
|
30152
|
+
* The only canonical category the Copilot CLI settings can express: URL
|
|
30153
|
+
* approvals. Every other category (`bash`, `edit`, `read`, ...) has no
|
|
30154
|
+
* user-authorable surface — the CLI's `permissions.allow`/`ask`/`deny` rule
|
|
30155
|
+
* arrays are accepted only in MDM/enterprise managed settings, and session
|
|
30156
|
+
* tool approvals are machine-written to `permissions-config.json`.
|
|
30157
|
+
*/
|
|
30158
|
+
const WEBFETCH_CATEGORY = "webfetch";
|
|
30159
|
+
/** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
|
|
30160
|
+
const ALLOWED_URLS_KEY = "allowedUrls";
|
|
30161
|
+
const DENIED_URLS_KEY = "deniedUrls";
|
|
30162
|
+
function toUrlList(value) {
|
|
30163
|
+
if (!Array.isArray(value)) return [];
|
|
30164
|
+
return value.filter((entry) => typeof entry === "string");
|
|
30165
|
+
}
|
|
30166
|
+
/**
|
|
30167
|
+
* Split one canonical category's rules into the two URL lists. `ask` entries
|
|
30168
|
+
* are omitted: the CLI prompts for any URL that is in neither list, which is
|
|
30169
|
+
* exactly what `ask` means.
|
|
30170
|
+
*/
|
|
30171
|
+
function splitUrlRules(rules) {
|
|
30172
|
+
const allowedUrls = [];
|
|
30173
|
+
const deniedUrls = [];
|
|
30174
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowedUrls.push(pattern);
|
|
30175
|
+
else if (action === "deny") deniedUrls.push(pattern);
|
|
30176
|
+
return {
|
|
30177
|
+
allowedUrls,
|
|
30178
|
+
deniedUrls
|
|
30179
|
+
};
|
|
30180
|
+
}
|
|
30181
|
+
/**
|
|
30182
|
+
* Permissions generator for the GitHub Copilot CLI.
|
|
30183
|
+
*
|
|
30184
|
+
* The CLI keeps two persistent settings files: the user-scope
|
|
30185
|
+
* `~/.copilot/settings.json` and the repository-scope
|
|
30186
|
+
* `.github/copilot/settings.json` (shipped in v1.0.60). Both are shared,
|
|
30187
|
+
* hand-edited files carrying unrelated keys (`model`, `effortLevel`, `hooks`,
|
|
30188
|
+
* `sandbox.*`, ...), so writes go through the shared-config gateway, only the
|
|
30189
|
+
* URL keys are owned, and the file is never deleted.
|
|
30190
|
+
*
|
|
30191
|
+
* The canonical `webfetch` category maps onto the CLI's two URL lists:
|
|
30192
|
+
* `allow` → `allowedUrls`, `deny` → `deniedUrls`, `ask` → the pattern is
|
|
30193
|
+
* omitted so the CLI falls through to its approval prompt.
|
|
30194
|
+
*
|
|
30195
|
+
* Scope asymmetry: the repository-scope key table documents `deniedUrls`
|
|
30196
|
+
* (union — a repository may add entries, never remove them) but NOT
|
|
30197
|
+
* `allowedUrls`, so an allow rule is only enforceable at user scope. At project
|
|
30198
|
+
* scope allow rules are therefore dropped with a warning rather than written to
|
|
30199
|
+
* a key the CLI ignores. (v1.0.79 additionally warns on startup about unknown
|
|
30200
|
+
* top-level keys in the user `settings.json`, so only documented keys are ever
|
|
30201
|
+
* emitted there either.)
|
|
30202
|
+
*
|
|
30203
|
+
* @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
|
|
30204
|
+
* @see https://github.com/github/copilot-cli/blob/main/changelog.md
|
|
30205
|
+
*/
|
|
30206
|
+
var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions {
|
|
30207
|
+
constructor(params) {
|
|
30208
|
+
super({
|
|
30209
|
+
...params,
|
|
30210
|
+
fileContent: params.fileContent ?? "{}"
|
|
30211
|
+
});
|
|
30212
|
+
}
|
|
30213
|
+
/**
|
|
30214
|
+
* `settings.json` holds unrelated user settings (`model`, `effortLevel`,
|
|
30215
|
+
* `hooks`, ...), so it must not be deleted.
|
|
30216
|
+
*/
|
|
30217
|
+
isDeletable() {
|
|
30218
|
+
return false;
|
|
30219
|
+
}
|
|
30220
|
+
static getSettablePaths({ global = false } = {}) {
|
|
30221
|
+
return global ? {
|
|
30222
|
+
relativeDirPath: COPILOT_DIR,
|
|
30223
|
+
relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
|
|
30224
|
+
} : {
|
|
30225
|
+
relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
|
|
30226
|
+
relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
|
|
30227
|
+
};
|
|
30228
|
+
}
|
|
30229
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
30230
|
+
const paths = CopilotcliPermissions.getSettablePaths({ global });
|
|
30231
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
30232
|
+
return new CopilotcliPermissions({
|
|
30233
|
+
outputRoot,
|
|
30234
|
+
relativeDirPath: paths.relativeDirPath,
|
|
30235
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30236
|
+
fileContent,
|
|
30237
|
+
validate,
|
|
30238
|
+
global
|
|
30239
|
+
});
|
|
30240
|
+
}
|
|
30241
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
|
|
30242
|
+
const paths = CopilotcliPermissions.getSettablePaths({ global });
|
|
30243
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
30244
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
30245
|
+
const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
|
|
30246
|
+
const patch = {};
|
|
30247
|
+
if (rules !== void 0) {
|
|
30248
|
+
const { allowedUrls, deniedUrls } = splitUrlRules(rules);
|
|
30249
|
+
patch[DENIED_URLS_KEY] = deniedUrls.length > 0 ? deniedUrls : void 0;
|
|
30250
|
+
if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
|
|
30251
|
+
else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${ALLOWED_URLS_KEY}" (repository settings may only add denials). Author allow rules in global scope (\`--global\`) instead.`);
|
|
30252
|
+
}
|
|
30253
|
+
return new CopilotcliPermissions({
|
|
30254
|
+
outputRoot,
|
|
30255
|
+
relativeDirPath: paths.relativeDirPath,
|
|
30256
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30257
|
+
fileContent: applySharedConfigPatch({
|
|
30258
|
+
fileKey: sharedConfigFileKey(paths),
|
|
30259
|
+
feature: "permissions",
|
|
30260
|
+
existingContent,
|
|
30261
|
+
patch,
|
|
30262
|
+
filePath
|
|
30263
|
+
}),
|
|
30264
|
+
validate: true,
|
|
30265
|
+
global
|
|
30266
|
+
});
|
|
30267
|
+
}
|
|
30268
|
+
toRulesyncPermissions() {
|
|
30269
|
+
const settings = this.parseSettings();
|
|
30270
|
+
const rules = {};
|
|
30271
|
+
for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
|
|
30272
|
+
if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
|
|
30273
|
+
const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
|
|
30274
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
30275
|
+
}
|
|
30276
|
+
parseSettings() {
|
|
30277
|
+
const relativePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
30278
|
+
try {
|
|
30279
|
+
return parseSharedConfig({
|
|
30280
|
+
format: "json",
|
|
30281
|
+
fileContent: this.getFileContent() || "{}",
|
|
30282
|
+
filePath: relativePath,
|
|
30283
|
+
invalidRootPolicy: "error"
|
|
30284
|
+
});
|
|
30285
|
+
} catch (error) {
|
|
30286
|
+
throw new Error(`Failed to parse Copilot CLI settings in ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
30287
|
+
}
|
|
30288
|
+
}
|
|
30289
|
+
validate() {
|
|
30290
|
+
return {
|
|
30291
|
+
success: true,
|
|
30292
|
+
error: null
|
|
30293
|
+
};
|
|
30294
|
+
}
|
|
30295
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
30296
|
+
return new CopilotcliPermissions({
|
|
30297
|
+
outputRoot,
|
|
30298
|
+
relativeDirPath,
|
|
30299
|
+
relativeFilePath,
|
|
30300
|
+
fileContent: "{}",
|
|
30301
|
+
validate: false,
|
|
30302
|
+
global
|
|
30303
|
+
});
|
|
30304
|
+
}
|
|
30305
|
+
};
|
|
30306
|
+
//#endregion
|
|
29215
30307
|
//#region src/features/permissions/cursor-permissions.ts
|
|
29216
30308
|
/**
|
|
29217
30309
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -30229,6 +31321,9 @@ const GROK_TOOL_TO_CATEGORY = {
|
|
|
30229
31321
|
WebSearch: "websearch"
|
|
30230
31322
|
};
|
|
30231
31323
|
const GROK_MCP_TOOL = "MCPTool";
|
|
31324
|
+
const GROK_TOOL_TO_CATEGORY_LOWER = Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category]));
|
|
31325
|
+
const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
|
|
31326
|
+
const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
30232
31327
|
/**
|
|
30233
31328
|
* Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
|
|
30234
31329
|
* `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
|
|
@@ -30281,6 +31376,44 @@ function parseGrokEntry(entry) {
|
|
|
30281
31376
|
};
|
|
30282
31377
|
}
|
|
30283
31378
|
/**
|
|
31379
|
+
* Parse one entry of the verbose `[[permission.rules]]` form
|
|
31380
|
+
* (`{ action = "allow", tool = "bash", pattern = "git *" }`) into a canonical
|
|
31381
|
+
* category + pattern + action. Unlike the compact array entries these are TOML
|
|
31382
|
+
* tables, so the tool name arrives as its own field instead of an entry string.
|
|
31383
|
+
* Returns `null` for malformed entries and for tools with no canonical
|
|
31384
|
+
* equivalent, so callers can skip them (the whole `rules` array is preserved
|
|
31385
|
+
* verbatim on generate regardless).
|
|
31386
|
+
*/
|
|
31387
|
+
function parseGrokRule(rule) {
|
|
31388
|
+
if (!isRecord$1(rule)) return null;
|
|
31389
|
+
const { action: rawAction, tool, pattern } = rule;
|
|
31390
|
+
const parsedAction = PermissionActionSchema.safeParse(rawAction);
|
|
31391
|
+
if (!parsedAction.success) return null;
|
|
31392
|
+
const action = parsedAction.data;
|
|
31393
|
+
if (typeof tool !== "string") return null;
|
|
31394
|
+
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
31395
|
+
const trimmedPattern = pattern?.trim() ?? "";
|
|
31396
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
|
|
31397
|
+
const lowerTool = tool.trim().toLowerCase();
|
|
31398
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
|
|
31399
|
+
category: "mcp",
|
|
31400
|
+
pattern: CATCH_ALL_PATTERN$2,
|
|
31401
|
+
action
|
|
31402
|
+
} : {
|
|
31403
|
+
category: `${MCP_CANONICAL_PREFIX$1}${resolvedPattern}`,
|
|
31404
|
+
pattern: CATCH_ALL_PATTERN$2,
|
|
31405
|
+
action
|
|
31406
|
+
};
|
|
31407
|
+
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
31408
|
+
const category = GROK_TOOL_TO_CATEGORY_LOWER[lowerTool];
|
|
31409
|
+
if (category === void 0) return null;
|
|
31410
|
+
return {
|
|
31411
|
+
category,
|
|
31412
|
+
pattern: resolvedPattern,
|
|
31413
|
+
action
|
|
31414
|
+
};
|
|
31415
|
+
}
|
|
31416
|
+
/**
|
|
30284
31417
|
* Permissions adapter for the xAI Grok Build CLI (`grokcli`).
|
|
30285
31418
|
*
|
|
30286
31419
|
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
@@ -30301,9 +31434,17 @@ function parseGrokEntry(entry) {
|
|
|
30301
31434
|
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
30302
31435
|
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
30303
31436
|
* entry never lands contradictorily in two arrays.
|
|
30304
|
-
* - Import:
|
|
30305
|
-
* categories
|
|
30306
|
-
*
|
|
31437
|
+
* - Import: both documented `[permission]` forms are parsed back into
|
|
31438
|
+
* canonical categories — the compact `allow`/`deny`/`ask` arrays and the
|
|
31439
|
+
* verbose `[[permission.rules]]` tables
|
|
31440
|
+
* (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
|
|
31441
|
+
* field is matched case-insensitively against the same tool table, plus
|
|
31442
|
+
* the documented `mcp` alias for the compact form's `MCPTool`. Rules
|
|
31443
|
+
* from the two forms are merged with the same `deny > ask > allow`
|
|
31444
|
+
* precedence. When neither form carries a rule (older configs), the coarse
|
|
31445
|
+
* `[ui] permission_mode` is used as a fallback. Generate always emits the
|
|
31446
|
+
* compact arrays, so a user-authored `rules` array is preserved verbatim
|
|
31447
|
+
* but not reconciled against them.
|
|
30307
31448
|
*
|
|
30308
31449
|
* The coarse `[ui] permission_mode` toggle is still written for backward
|
|
30309
31450
|
* compatibility with older Grok versions: `always-approve` when the config is
|
|
@@ -30470,29 +31611,44 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
|
30470
31611
|
};
|
|
30471
31612
|
}
|
|
30472
31613
|
/**
|
|
30473
|
-
* Parse Grok's `[permission]`
|
|
30474
|
-
*
|
|
30475
|
-
*
|
|
30476
|
-
*
|
|
30477
|
-
*
|
|
31614
|
+
* Parse Grok's `[permission]` section back into a canonical permission map,
|
|
31615
|
+
* reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
|
|
31616
|
+
* Claude-style entries and the verbose `[[permission.rules]]` tables
|
|
31617
|
+
* (`{ action, tool, pattern }`). Returns `null` only when neither form carries
|
|
31618
|
+
* any rule, so the caller can fall back to the coarse `permission_mode`.
|
|
31619
|
+
* Precedence `deny > ask > allow` is applied across both forms, so a tool
|
|
31620
|
+
* described more than once resolves to the strictest action.
|
|
30478
31621
|
*/
|
|
30479
31622
|
function parseGrokPermissionArrays(permission) {
|
|
30480
31623
|
const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
|
|
30481
31624
|
const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
|
|
30482
31625
|
const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
|
|
30483
|
-
|
|
31626
|
+
const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
|
|
31627
|
+
if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
|
|
30484
31628
|
const result = {};
|
|
30485
|
-
const
|
|
31629
|
+
const record = ({ category, pattern, action }) => {
|
|
31630
|
+
const bucket = result[category] ??= {};
|
|
31631
|
+
const existing = bucket[pattern];
|
|
31632
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) bucket[pattern] = action;
|
|
31633
|
+
};
|
|
31634
|
+
const applyEntries = (entries, action) => {
|
|
30486
31635
|
for (const entry of entries ?? []) {
|
|
30487
31636
|
const parsed = parseGrokEntry(entry);
|
|
30488
31637
|
if (parsed === null) continue;
|
|
30489
|
-
|
|
30490
|
-
|
|
31638
|
+
record({
|
|
31639
|
+
...parsed,
|
|
31640
|
+
action
|
|
31641
|
+
});
|
|
30491
31642
|
}
|
|
30492
31643
|
};
|
|
30493
|
-
|
|
30494
|
-
|
|
30495
|
-
|
|
31644
|
+
applyEntries(allow, "allow");
|
|
31645
|
+
applyEntries(ask, "ask");
|
|
31646
|
+
applyEntries(deny, "deny");
|
|
31647
|
+
for (const rule of rules ?? []) {
|
|
31648
|
+
const parsed = parseGrokRule(rule);
|
|
31649
|
+
if (parsed === null) continue;
|
|
31650
|
+
record(parsed);
|
|
31651
|
+
}
|
|
30496
31652
|
return result;
|
|
30497
31653
|
}
|
|
30498
31654
|
/**
|
|
@@ -33177,6 +34333,13 @@ const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
|
|
|
33177
34333
|
const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
|
|
33178
34334
|
const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
|
|
33179
34335
|
const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
|
|
34336
|
+
const TAKT_RUNTIME_PROVIDER_KEY = "provider";
|
|
34337
|
+
const TAKT_RUNTIME_DEFAULTS_KEY = "defaults";
|
|
34338
|
+
const TAKT_RUNTIME_PROFILES_KEY = "profiles";
|
|
34339
|
+
const TAKT_RUNTIME_TARGETS_KEY = "targets";
|
|
34340
|
+
const TAKT_RUNTIME_AUTO_ROUTING_KEY = "auto_routing";
|
|
34341
|
+
const TAKT_RUNTIME_PROFILE_KEY = "profile";
|
|
34342
|
+
const TAKT_RUNTIME_OPTIONS_KEY = "options";
|
|
33180
34343
|
const TAKT_SECURITY_POLICIES = {
|
|
33181
34344
|
workflow_arpeggio: [
|
|
33182
34345
|
"custom_data_source_modules",
|
|
@@ -33229,13 +34392,36 @@ const CATCH_ALL_PATTERN = "*";
|
|
|
33229
34392
|
* profile, layered on top of `default_permission_mode`) and `provider_options`
|
|
33230
34393
|
* (a top-level per-provider sandbox/network table). Both are authored on
|
|
33231
34394
|
* generate and re-extracted on import.
|
|
34395
|
+
*
|
|
34396
|
+
* Takt 0.56.0 moved provider configuration into a separate `runtime.yaml`
|
|
34397
|
+
* (`.takt/runtime.yaml`, `~/.takt/runtime.yaml`). Once its `provider:` section
|
|
34398
|
+
* carries an assignment ("runtime mode" — the state a freshly installed Takt is
|
|
34399
|
+
* in, since it generates an active global runtime.yaml on first launch), any
|
|
34400
|
+
* legacy provider setting left in `config.yaml` makes Takt stop before running
|
|
34401
|
+
* an agent with "Mixed provider configuration detected". rulesync only reads
|
|
34402
|
+
* that file, in two places:
|
|
34403
|
+
* - the active provider is resolved from it first (the `provider_profiles`
|
|
34404
|
+
* key that carries the permission mode is still keyed by provider name, and
|
|
34405
|
+
* `provider_profiles` is not itself a legacy signal);
|
|
34406
|
+
* - an authored `provider_options` is refused with a warning rather than
|
|
34407
|
+
* written while runtime mode is active, and its runtime-side counterpart
|
|
34408
|
+
* (`provider.profiles.*.options`) is lifted back out on import.
|
|
34409
|
+
* Installs with no runtime.yaml, or an inactive one, keep the legacy behavior
|
|
34410
|
+
* unchanged.
|
|
33232
34411
|
*/
|
|
33233
34412
|
var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
33234
|
-
|
|
34413
|
+
/**
|
|
34414
|
+
* The sibling `runtime.yaml` as read from the same scope, or `""` when the
|
|
34415
|
+
* install has none. Import needs it — `toRulesyncPermissions()` is
|
|
34416
|
+
* synchronous, so the file is read alongside config.yaml in `fromFile()`.
|
|
34417
|
+
*/
|
|
34418
|
+
runtimeFileContent;
|
|
34419
|
+
constructor({ runtimeFileContent, ...params }) {
|
|
33235
34420
|
super({
|
|
33236
34421
|
...params,
|
|
33237
34422
|
fileContent: params.fileContent ?? ""
|
|
33238
34423
|
});
|
|
34424
|
+
this.runtimeFileContent = runtimeFileContent ?? "";
|
|
33239
34425
|
}
|
|
33240
34426
|
isDeletable() {
|
|
33241
34427
|
return false;
|
|
@@ -33249,11 +34435,13 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33249
34435
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
33250
34436
|
const paths = TaktPermissions.getSettablePaths({ global });
|
|
33251
34437
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
34438
|
+
const runtimeFileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, "runtime.yaml")) ?? "";
|
|
33252
34439
|
return new TaktPermissions({
|
|
33253
34440
|
outputRoot,
|
|
33254
34441
|
relativeDirPath: paths.relativeDirPath,
|
|
33255
34442
|
relativeFilePath: paths.relativeFilePath,
|
|
33256
34443
|
fileContent,
|
|
34444
|
+
runtimeFileContent,
|
|
33257
34445
|
validate,
|
|
33258
34446
|
global
|
|
33259
34447
|
});
|
|
@@ -33268,12 +34456,22 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33268
34456
|
filePath,
|
|
33269
34457
|
invalidRootPolicy: "error"
|
|
33270
34458
|
});
|
|
34459
|
+
const runtime = await readTaktRuntimeConfig({
|
|
34460
|
+
outputRoot,
|
|
34461
|
+
logger
|
|
34462
|
+
});
|
|
33271
34463
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
33272
|
-
const provider = resolveActiveProvider(
|
|
34464
|
+
const provider = resolveActiveProvider({
|
|
34465
|
+
config,
|
|
34466
|
+
runtime: runtime.active ? runtime.config : void 0
|
|
34467
|
+
});
|
|
33273
34468
|
const mode = deriveTaktPermissionMode(rulesyncJson);
|
|
33274
34469
|
const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
|
|
33275
34470
|
const stepOverrides = isPlainObject$1(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
|
|
33276
|
-
const
|
|
34471
|
+
const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
|
|
34472
|
+
const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
|
|
34473
|
+
if (runtimeModeRefusesProviderOptions) logger?.warn(`Takt permissions: not writing "${TAKT_PROVIDER_OPTIONS_KEY}" to ${filePath} because ${runtime.filePaths.join(" + ")} puts Takt in runtime provider mode (Takt 0.56.0+), where any legacy provider setting in config.yaml makes Takt fail with "Mixed provider configuration detected". Move those options to \`provider.profiles.<profile>.options\` in runtime.yaml, and remove them from the \`takt\` block of the rulesync source. A "${TAKT_PROVIDER_OPTIONS_KEY}" already in ${filePath} is left untouched — rulesync does not own that key.`);
|
|
34474
|
+
const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
|
|
33277
34475
|
const authoredPolicies = pickSecurityPolicies(override, {
|
|
33278
34476
|
filePath,
|
|
33279
34477
|
logger,
|
|
@@ -33319,13 +34517,26 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33319
34517
|
filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
|
|
33320
34518
|
invalidRootPolicy: "error"
|
|
33321
34519
|
});
|
|
33322
|
-
const
|
|
34520
|
+
const runtimeFilePath = (0, node_path.join)(this.getRelativeDirPath(), TAKT_RUNTIME_CONFIG_FILE_NAME);
|
|
34521
|
+
const runtime = parseSharedConfig({
|
|
34522
|
+
format: "yaml",
|
|
34523
|
+
fileContent: this.runtimeFileContent,
|
|
34524
|
+
filePath: runtimeFilePath
|
|
34525
|
+
});
|
|
34526
|
+
const runtimeActive = isRuntimeModeActive(runtime);
|
|
34527
|
+
const provider = resolveActiveProvider({
|
|
34528
|
+
config,
|
|
34529
|
+
runtime: runtimeActive ? runtime : void 0
|
|
34530
|
+
});
|
|
33323
34531
|
const profiles = isPlainObject$1(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
|
|
33324
34532
|
const profile = isPlainObject$1(profiles[provider]) ? profiles[provider] : {};
|
|
33325
34533
|
const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
|
|
33326
34534
|
const rulesyncConfig = taktModeToRulesyncConfig(mode);
|
|
33327
34535
|
const stepOverrides = isPlainObject$1(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
|
|
33328
|
-
const providerOptions =
|
|
34536
|
+
const providerOptions = mergeSharedConfigDeep({
|
|
34537
|
+
base: isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : {},
|
|
34538
|
+
patch: runtimeActive ? collectRuntimeProviderOptions(runtime) : {}
|
|
34539
|
+
});
|
|
33329
34540
|
const taktOverride = {};
|
|
33330
34541
|
if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
|
|
33331
34542
|
if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
|
|
@@ -33352,10 +34563,18 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33352
34563
|
}
|
|
33353
34564
|
};
|
|
33354
34565
|
/**
|
|
33355
|
-
* Resolve the active Takt provider
|
|
33356
|
-
*
|
|
34566
|
+
* Resolve the active Takt provider.
|
|
34567
|
+
*
|
|
34568
|
+
* Under runtime mode (Takt 0.56.0+) the real provider assignment lives in
|
|
34569
|
+
* `runtime.yaml`, and `config.yaml:provider` cannot coexist with it, so the
|
|
34570
|
+
* runtime document is consulted first: the provider of the profile named by
|
|
34571
|
+
* `provider.defaults.profile`. Everything else falls through to the legacy
|
|
34572
|
+
* chain — the top-level `provider:` value, else the sole key in
|
|
34573
|
+
* `provider_profiles`, else the `claude` default.
|
|
33357
34574
|
*/
|
|
33358
|
-
function resolveActiveProvider(config) {
|
|
34575
|
+
function resolveActiveProvider({ config, runtime }) {
|
|
34576
|
+
const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
|
|
34577
|
+
if (fromRuntime !== void 0) return fromRuntime;
|
|
33359
34578
|
if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
|
|
33360
34579
|
const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
|
|
33361
34580
|
if (isPlainObject$1(profiles)) {
|
|
@@ -33364,6 +34583,170 @@ function resolveActiveProvider(config) {
|
|
|
33364
34583
|
}
|
|
33365
34584
|
return TAKT_DEFAULT_PROVIDER;
|
|
33366
34585
|
}
|
|
34586
|
+
/** A plain object carrying at least one entry (Takt's "assignment" test). */
|
|
34587
|
+
function hasEntries(value) {
|
|
34588
|
+
return isPlainObject$1(value) && Object.keys(value).length > 0;
|
|
34589
|
+
}
|
|
34590
|
+
function runtimeProfiles(runtime) {
|
|
34591
|
+
const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34592
|
+
if (!isPlainObject$1(provider)) return {};
|
|
34593
|
+
const profiles = provider[TAKT_RUNTIME_PROFILES_KEY];
|
|
34594
|
+
return isPlainObject$1(profiles) ? profiles : {};
|
|
34595
|
+
}
|
|
34596
|
+
/**
|
|
34597
|
+
* Whether a parsed `runtime.yaml` puts Takt into runtime mode, mirroring
|
|
34598
|
+
* upstream's mode detection: the `provider:` section must carry an actual
|
|
34599
|
+
* assignment — a non-empty `defaults`, `profiles` or `auto_routing`, or a
|
|
34600
|
+
* `targets` map with at least one non-empty nested map. The file existing is not
|
|
34601
|
+
* enough, and empty nested maps (`defaults: {}`, `targets: { personas: {} }`)
|
|
34602
|
+
* must not flip the mode.
|
|
34603
|
+
* https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/mode.ts
|
|
34604
|
+
*/
|
|
34605
|
+
function isRuntimeModeActive(runtime) {
|
|
34606
|
+
const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34607
|
+
if (!isPlainObject$1(provider)) return false;
|
|
34608
|
+
if (hasEntries(provider[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(provider[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(provider[TAKT_RUNTIME_AUTO_ROUTING_KEY])) return true;
|
|
34609
|
+
const targets = provider[TAKT_RUNTIME_TARGETS_KEY];
|
|
34610
|
+
return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
|
|
34611
|
+
}
|
|
34612
|
+
/** The provider named by the runtime profile Takt would use by default. */
|
|
34613
|
+
function resolveRuntimeProvider(runtime) {
|
|
34614
|
+
const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34615
|
+
const profiles = runtimeProfiles(runtime);
|
|
34616
|
+
const providerOf = (profileName) => {
|
|
34617
|
+
if (profileName === void 0) return;
|
|
34618
|
+
const profile = profiles[profileName];
|
|
34619
|
+
if (!isPlainObject$1(profile)) return;
|
|
34620
|
+
const value = profile[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34621
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
34622
|
+
};
|
|
34623
|
+
if (!isPlainObject$1(provider)) return;
|
|
34624
|
+
const defaults = provider[TAKT_RUNTIME_DEFAULTS_KEY];
|
|
34625
|
+
if (!isPlainObject$1(defaults) || typeof defaults[TAKT_RUNTIME_PROFILE_KEY] !== "string") return;
|
|
34626
|
+
return providerOf(defaults[TAKT_RUNTIME_PROFILE_KEY]);
|
|
34627
|
+
}
|
|
34628
|
+
/**
|
|
34629
|
+
* Lift the runtime profiles' flat `options` bags back into the per-provider
|
|
34630
|
+
* shape the `takt` override uses. Each profile's options belong to that
|
|
34631
|
+
* profile's own provider; when several profiles name the same provider they are
|
|
34632
|
+
* merged in document order, so a later profile wins on a colliding option key.
|
|
34633
|
+
*/
|
|
34634
|
+
function collectRuntimeProviderOptions(runtime) {
|
|
34635
|
+
const collected = {};
|
|
34636
|
+
for (const profile of Object.values(runtimeProfiles(runtime))) {
|
|
34637
|
+
if (!isPlainObject$1(profile)) continue;
|
|
34638
|
+
const providerName = profile[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34639
|
+
const options = profile[TAKT_RUNTIME_OPTIONS_KEY];
|
|
34640
|
+
if (typeof providerName !== "string" || providerName.trim() === "" || !hasEntries(options)) continue;
|
|
34641
|
+
const existing = collected[providerName];
|
|
34642
|
+
collected[providerName] = {
|
|
34643
|
+
...isPlainObject$1(existing) ? existing : {},
|
|
34644
|
+
...options
|
|
34645
|
+
};
|
|
34646
|
+
}
|
|
34647
|
+
return collected;
|
|
34648
|
+
}
|
|
34649
|
+
/**
|
|
34650
|
+
* Read one `runtime.yaml`. A file that cannot be parsed reports `unparsable`
|
|
34651
|
+
* rather than a document: Takt refuses to start on a broken runtime.yaml, and
|
|
34652
|
+
* treating it as legacy would be the one outcome that writes the
|
|
34653
|
+
* mixed-configuration key into the user's config.yaml.
|
|
34654
|
+
*/
|
|
34655
|
+
async function readTaktRuntimeFile({ filePath, logger }) {
|
|
34656
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
34657
|
+
if (fileContent === null) return;
|
|
34658
|
+
try {
|
|
34659
|
+
return {
|
|
34660
|
+
config: parseSharedConfig({
|
|
34661
|
+
format: "yaml",
|
|
34662
|
+
fileContent,
|
|
34663
|
+
filePath
|
|
34664
|
+
}),
|
|
34665
|
+
unparsable: false
|
|
34666
|
+
};
|
|
34667
|
+
} catch (error) {
|
|
34668
|
+
logger?.warn(`Takt permissions: could not parse ${filePath} (${formatError(error)}); assuming Takt's runtime provider mode is active so no legacy provider setting is written to config.yaml.`);
|
|
34669
|
+
return {
|
|
34670
|
+
config: {},
|
|
34671
|
+
unparsable: true
|
|
34672
|
+
};
|
|
34673
|
+
}
|
|
34674
|
+
}
|
|
34675
|
+
/**
|
|
34676
|
+
* Collapse the project and global `runtime.yaml` into the single document Takt
|
|
34677
|
+
* itself resolves against, matching upstream's loader: `profiles` is a union
|
|
34678
|
+
* with the project definition of a same-named profile replacing the global one,
|
|
34679
|
+
* while `defaults`, `targets` and `auto_routing` are taken from the project file
|
|
34680
|
+
* whole whenever it states them at all (`??`, so a project `targets: {}` masks
|
|
34681
|
+
* the global one rather than merging with it).
|
|
34682
|
+
* https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/loader.ts
|
|
34683
|
+
*
|
|
34684
|
+
* Merging before mode detection matters in both directions: a project file
|
|
34685
|
+
* active only through `targets:` still resolves its provider from the global
|
|
34686
|
+
* file's `defaults`/`profiles`, and a project section that masks the global one
|
|
34687
|
+
* leaves the merged document inactive even though the global file alone was not.
|
|
34688
|
+
*/
|
|
34689
|
+
function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
|
|
34690
|
+
const sectionOf = (config) => {
|
|
34691
|
+
const provider = config?.[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34692
|
+
return isPlainObject$1(provider) ? provider : {};
|
|
34693
|
+
};
|
|
34694
|
+
const projectSection = sectionOf(project);
|
|
34695
|
+
const globalSection = sectionOf(globalConfig);
|
|
34696
|
+
const replaced = (key) => projectSection[key] ?? globalSection[key];
|
|
34697
|
+
const profilesOf = (section) => isPlainObject$1(section[TAKT_RUNTIME_PROFILES_KEY]) ? section[TAKT_RUNTIME_PROFILES_KEY] : void 0;
|
|
34698
|
+
const globalProfiles = profilesOf(globalSection);
|
|
34699
|
+
const projectProfiles = profilesOf(projectSection);
|
|
34700
|
+
const provider = {};
|
|
34701
|
+
if (globalProfiles !== void 0 || projectProfiles !== void 0) provider[TAKT_RUNTIME_PROFILES_KEY] = {
|
|
34702
|
+
...globalProfiles,
|
|
34703
|
+
...projectProfiles
|
|
34704
|
+
};
|
|
34705
|
+
for (const key of [
|
|
34706
|
+
TAKT_RUNTIME_DEFAULTS_KEY,
|
|
34707
|
+
TAKT_RUNTIME_TARGETS_KEY,
|
|
34708
|
+
TAKT_RUNTIME_AUTO_ROUTING_KEY
|
|
34709
|
+
]) {
|
|
34710
|
+
const value = replaced(key);
|
|
34711
|
+
if (value !== void 0) provider[key] = value;
|
|
34712
|
+
}
|
|
34713
|
+
return { [TAKT_RUNTIME_PROVIDER_KEY]: provider };
|
|
34714
|
+
}
|
|
34715
|
+
/**
|
|
34716
|
+
* The runtime provider configuration in force for a generate run: the project
|
|
34717
|
+
* and global `runtime.yaml` merged the way Takt merges them, plus whether the
|
|
34718
|
+
* result puts Takt in runtime mode.
|
|
34719
|
+
*
|
|
34720
|
+
* Both scopes are read whichever scope is being generated: Takt collects legacy
|
|
34721
|
+
* signals from the project and global `config.yaml` alike, so a global
|
|
34722
|
+
* `runtime.yaml` — the one Takt generates on first launch — makes a legacy key
|
|
34723
|
+
* in the project config.yaml a hard failure just the same.
|
|
34724
|
+
*/
|
|
34725
|
+
async function readTaktRuntimeConfig({ outputRoot, logger }) {
|
|
34726
|
+
const projectPath = (0, node_path.join)(outputRoot, TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
|
|
34727
|
+
let globalPath;
|
|
34728
|
+
try {
|
|
34729
|
+
const resolved = (0, node_path.join)(getHomeDirectory(), TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
|
|
34730
|
+
globalPath = resolved === projectPath ? void 0 : resolved;
|
|
34731
|
+
} catch {}
|
|
34732
|
+
const [project, globalFile] = await Promise.all([readTaktRuntimeFile({
|
|
34733
|
+
filePath: projectPath,
|
|
34734
|
+
logger
|
|
34735
|
+
}), globalPath === void 0 ? Promise.resolve(void 0) : readTaktRuntimeFile({
|
|
34736
|
+
filePath: globalPath,
|
|
34737
|
+
logger
|
|
34738
|
+
})]);
|
|
34739
|
+
const filePaths = [...project === void 0 ? [] : [projectPath], ...globalFile === void 0 || globalPath === void 0 ? [] : [globalPath]];
|
|
34740
|
+
const config = mergeTaktRuntimeConfigs({
|
|
34741
|
+
project: project?.config,
|
|
34742
|
+
global: globalFile?.config
|
|
34743
|
+
});
|
|
34744
|
+
return {
|
|
34745
|
+
filePaths,
|
|
34746
|
+
config,
|
|
34747
|
+
active: project?.unparsable === true || globalFile?.unparsable === true || isRuntimeModeActive(config)
|
|
34748
|
+
};
|
|
34749
|
+
}
|
|
33367
34750
|
/**
|
|
33368
34751
|
* Collapse a rulesync permissions config into Takt's single coarse mode.
|
|
33369
34752
|
*
|
|
@@ -34223,6 +35606,57 @@ function asRecord(value) {
|
|
|
34223
35606
|
return Object.fromEntries(Object.entries(value));
|
|
34224
35607
|
}
|
|
34225
35608
|
/**
|
|
35609
|
+
* The `agent` keys the `zed` override may author. Everything else — above all
|
|
35610
|
+
* `tool_permissions`, which the canonical `permission` block owns end to end —
|
|
35611
|
+
* is refused, so an override can never reach past its own surface and weaken a
|
|
35612
|
+
* canonical deny. The override object is read key by key rather than spread, so
|
|
35613
|
+
* an unlisted key is inert whether or not it is named here — an allowlist, not a
|
|
35614
|
+
* denylist, because `agent` carries blunt instruments of its own (Zed's
|
|
35615
|
+
* `always_allow_tool_actions` would disarm every permission rule at once), and a
|
|
35616
|
+
* verbatim `agent` merge would hand them to the override.
|
|
35617
|
+
*
|
|
35618
|
+
* Every key the patch does not consume is reported, so an unsupported or
|
|
35619
|
+
* misspelled one surfaces as a warning rather than as config that quietly does
|
|
35620
|
+
* nothing. `permission` is exempt: it is the canonical tool-scoped block, and
|
|
35621
|
+
* `RulesyncPermissions.forTarget` has already consumed and stripped it.
|
|
35622
|
+
*/
|
|
35623
|
+
const ZED_OVERRIDE_AGENT_KEYS = ["sandbox_permissions", "profiles"];
|
|
35624
|
+
const ZED_CANONICAL_AGENT_KEY = "tool_permissions";
|
|
35625
|
+
const ZED_OVERRIDE_CONSUMED_KEYS = /* @__PURE__ */ new Set([...ZED_OVERRIDE_AGENT_KEYS, "permission"]);
|
|
35626
|
+
/**
|
|
35627
|
+
* Build the `agent` patch fragment carrying the `zed` override's verbatim
|
|
35628
|
+
* blocks. A key the override supplies replaces the existing block wholesale
|
|
35629
|
+
* (Zed reads each as one unit — a deep merge would leave half of a rewritten
|
|
35630
|
+
* sandbox policy behind); a key it omits is left out of the fragment, so the
|
|
35631
|
+
* existing value survives via the caller's spread of `agent`.
|
|
35632
|
+
*/
|
|
35633
|
+
function buildZedOverridePatch({ override, logger }) {
|
|
35634
|
+
if (!isPlainObject$1(override)) return {};
|
|
35635
|
+
if (ZED_CANONICAL_AGENT_KEY in override) logger?.warn(`Zed permissions: ignoring the 'zed.${ZED_CANONICAL_AGENT_KEY}' override; \`agent.${ZED_CANONICAL_AGENT_KEY}\` is driven by the canonical permission block.`);
|
|
35636
|
+
const unsupportedKeys = Object.keys(override).filter((key) => key !== ZED_CANONICAL_AGENT_KEY && !ZED_OVERRIDE_CONSUMED_KEYS.has(key));
|
|
35637
|
+
if (unsupportedKeys.length > 0) logger?.warn(`Zed permissions: ignoring the ${unsupportedKeys.map((key) => `'zed.${key}'`).join(", ")} override ${unsupportedKeys.length === 1 ? "key" : "keys"} — the \`zed\` block authors only ${ZED_OVERRIDE_AGENT_KEYS.map((key) => `\`${key}\``).join(" and ")}.`);
|
|
35638
|
+
const patch = {};
|
|
35639
|
+
for (const key of ZED_OVERRIDE_AGENT_KEYS) {
|
|
35640
|
+
const value = override[key];
|
|
35641
|
+
if (isPlainObject$1(value)) patch[key] = value;
|
|
35642
|
+
}
|
|
35643
|
+
return patch;
|
|
35644
|
+
}
|
|
35645
|
+
/**
|
|
35646
|
+
* The write-side inverse: lift `agent.sandbox_permissions` / `agent.profiles`
|
|
35647
|
+
* back into the `zed` override so a hand-written sandbox policy or profile set
|
|
35648
|
+
* round-trips instead of being lost on the next generate. Returns `undefined`
|
|
35649
|
+
* when the settings carry neither, so the override key is omitted.
|
|
35650
|
+
*/
|
|
35651
|
+
function extractZedOverride(agent) {
|
|
35652
|
+
const override = {};
|
|
35653
|
+
for (const key of ZED_OVERRIDE_AGENT_KEYS) {
|
|
35654
|
+
const value = agent[key];
|
|
35655
|
+
if (isPlainObject$1(value)) override[key] = value;
|
|
35656
|
+
}
|
|
35657
|
+
return Object.keys(override).length > 0 ? override : void 0;
|
|
35658
|
+
}
|
|
35659
|
+
/**
|
|
34226
35660
|
* Permissions generator for the Zed editor.
|
|
34227
35661
|
*
|
|
34228
35662
|
* Zed maps tool permissions onto `agent.tool_permissions` inside its settings
|
|
@@ -34230,6 +35664,11 @@ function asRecord(value) {
|
|
|
34230
35664
|
* global). That file is shared with the MCP (`context_servers`) and ignore
|
|
34231
35665
|
* (`private_files`) features, so reads and writes merge into the existing JSON
|
|
34232
35666
|
* rather than overwriting it, and the file is never deleted.
|
|
35667
|
+
*
|
|
35668
|
+
* Zed's OS sandbox (`agent.sandbox_permissions`) and its tool-availability
|
|
35669
|
+
* profiles (`agent.profiles`) are separate enforcement layers with no canonical
|
|
35670
|
+
* counterpart; they are authored verbatim through the `zed` override and lifted
|
|
35671
|
+
* back out of the settings on import. See `ZedPermissionsOverrideSchema`.
|
|
34233
35672
|
*/
|
|
34234
35673
|
var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
34235
35674
|
constructor(params) {
|
|
@@ -34308,6 +35747,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
34308
35747
|
existingContent,
|
|
34309
35748
|
patch: { agent: {
|
|
34310
35749
|
...agent,
|
|
35750
|
+
...buildZedOverridePatch({
|
|
35751
|
+
override: config.zed,
|
|
35752
|
+
logger
|
|
35753
|
+
}),
|
|
34311
35754
|
tool_permissions: {
|
|
34312
35755
|
...toolPermissions,
|
|
34313
35756
|
...managedDefault !== void 0 && { default: managedDefault },
|
|
@@ -34329,7 +35772,8 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
34329
35772
|
} catch (error) {
|
|
34330
35773
|
throw new Error(`Failed to parse Zed permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
34331
35774
|
}
|
|
34332
|
-
const
|
|
35775
|
+
const agent = asRecord(settings.agent);
|
|
35776
|
+
const toolPermissionsRaw = agent.tool_permissions;
|
|
34333
35777
|
const parsed = ZedToolPermissionsSchema.safeParse(toolPermissionsRaw ?? {});
|
|
34334
35778
|
const tools = parsed.success ? parsed.data.tools ?? {} : {};
|
|
34335
35779
|
const globalDefault = parsed.success ? parsed.data.default : void 0;
|
|
@@ -34353,7 +35797,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
34353
35797
|
for (const entry of toolPermission.always_deny ?? []) ensure(category)[entry.pattern] = "deny";
|
|
34354
35798
|
for (const entry of toolPermission.always_confirm ?? []) ensure(category)[entry.pattern] = "ask";
|
|
34355
35799
|
}
|
|
34356
|
-
|
|
35800
|
+
const zedOverride = extractZedOverride(agent);
|
|
35801
|
+
const result = { permission };
|
|
35802
|
+
if (zedOverride !== void 0) result.zed = zedOverride;
|
|
35803
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
34357
35804
|
}
|
|
34358
35805
|
validate() {
|
|
34359
35806
|
return {
|
|
@@ -34439,6 +35886,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
34439
35886
|
supportsImport: true
|
|
34440
35887
|
}
|
|
34441
35888
|
}],
|
|
35889
|
+
["copilotcli", {
|
|
35890
|
+
class: CopilotcliPermissions,
|
|
35891
|
+
meta: {
|
|
35892
|
+
supportsProject: true,
|
|
35893
|
+
supportsGlobal: true,
|
|
35894
|
+
supportsImport: true
|
|
35895
|
+
}
|
|
35896
|
+
}],
|
|
34442
35897
|
["cursor", {
|
|
34443
35898
|
class: CursorPermissions,
|
|
34444
35899
|
meta: {
|
|
@@ -36392,7 +37847,8 @@ function openaiYamlToCodexcliSection(parsed) {
|
|
|
36392
37847
|
/**
|
|
36393
37848
|
* Represents a Codex CLI skill directory.
|
|
36394
37849
|
* Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
|
|
36395
|
-
* and global mode (under
|
|
37850
|
+
* and global mode (under ~/.agents/skills — Rulesync resolves the home
|
|
37851
|
+
* directory rather than $CODEX_HOME).
|
|
36396
37852
|
*/
|
|
36397
37853
|
var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
36398
37854
|
constructor({ outputRoot = process.cwd(), relativeDirPath = CODEXCLI_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -38428,6 +39884,145 @@ var KiroIdeSkill = class extends KiroSkill {
|
|
|
38428
39884
|
}
|
|
38429
39885
|
};
|
|
38430
39886
|
//#endregion
|
|
39887
|
+
//#region src/features/skills/musecode-skill.ts
|
|
39888
|
+
const MusecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
39889
|
+
name: zod_mini.z.string(),
|
|
39890
|
+
description: zod_mini.z.string()
|
|
39891
|
+
});
|
|
39892
|
+
/**
|
|
39893
|
+
* Represents a Meta Muse Code skill directory.
|
|
39894
|
+
*
|
|
39895
|
+
* Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
|
|
39896
|
+
* project (`.agents/skills/`) and from the user config
|
|
39897
|
+
* (`$XDG_CONFIG_HOME/muse/skills`, plus `~/.agents/skills`). rulesync emits the
|
|
39898
|
+
* project directory and, at global scope, only the XDG-default
|
|
39899
|
+
* `~/.config/muse/skills` so a skill is written exactly once. Muse Code's
|
|
39900
|
+
* compat scans of repo-local `.codex/skills` and `.claude/skills` belong to
|
|
39901
|
+
* other tools and are not emitted.
|
|
39902
|
+
* @see https://dev.meta.ai/docs/muse-code/extending.md
|
|
39903
|
+
*/
|
|
39904
|
+
var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
39905
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = MUSECODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
39906
|
+
super({
|
|
39907
|
+
outputRoot,
|
|
39908
|
+
relativeDirPath,
|
|
39909
|
+
dirName,
|
|
39910
|
+
mainFile: {
|
|
39911
|
+
name: SKILL_FILE_NAME,
|
|
39912
|
+
body,
|
|
39913
|
+
frontmatter: { ...frontmatter }
|
|
39914
|
+
},
|
|
39915
|
+
otherFiles,
|
|
39916
|
+
global
|
|
39917
|
+
});
|
|
39918
|
+
if (validate) {
|
|
39919
|
+
const result = this.validate();
|
|
39920
|
+
if (!result.success) throw result.error;
|
|
39921
|
+
}
|
|
39922
|
+
}
|
|
39923
|
+
static getSettablePaths({ global = false } = {}) {
|
|
39924
|
+
return { relativeDirPath: global ? MUSECODE_GLOBAL_SKILLS_DIR_PATH : MUSECODE_SKILLS_DIR_PATH };
|
|
39925
|
+
}
|
|
39926
|
+
getFrontmatter() {
|
|
39927
|
+
return MusecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
39928
|
+
}
|
|
39929
|
+
getBody() {
|
|
39930
|
+
return this.mainFile?.body ?? "";
|
|
39931
|
+
}
|
|
39932
|
+
validate() {
|
|
39933
|
+
if (!this.mainFile) return {
|
|
39934
|
+
success: false,
|
|
39935
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
39936
|
+
};
|
|
39937
|
+
const result = MusecodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
39938
|
+
if (!result.success) return {
|
|
39939
|
+
success: false,
|
|
39940
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
39941
|
+
};
|
|
39942
|
+
return {
|
|
39943
|
+
success: true,
|
|
39944
|
+
error: null
|
|
39945
|
+
};
|
|
39946
|
+
}
|
|
39947
|
+
toRulesyncSkill() {
|
|
39948
|
+
const frontmatter = this.getFrontmatter();
|
|
39949
|
+
const rulesyncFrontmatter = {
|
|
39950
|
+
name: frontmatter.name,
|
|
39951
|
+
description: frontmatter.description,
|
|
39952
|
+
targets: ["*"]
|
|
39953
|
+
};
|
|
39954
|
+
return new RulesyncSkill({
|
|
39955
|
+
outputRoot: this.outputRoot,
|
|
39956
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
39957
|
+
dirName: this.getDirName(),
|
|
39958
|
+
frontmatter: rulesyncFrontmatter,
|
|
39959
|
+
body: this.getBody(),
|
|
39960
|
+
otherFiles: this.getOtherFiles(),
|
|
39961
|
+
validate: true,
|
|
39962
|
+
global: this.global
|
|
39963
|
+
});
|
|
39964
|
+
}
|
|
39965
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
39966
|
+
const settablePaths = MusecodeSkill.getSettablePaths({ global });
|
|
39967
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
39968
|
+
const musecodeFrontmatter = {
|
|
39969
|
+
name: rulesyncFrontmatter.name,
|
|
39970
|
+
description: rulesyncFrontmatter.description
|
|
39971
|
+
};
|
|
39972
|
+
return new MusecodeSkill({
|
|
39973
|
+
outputRoot,
|
|
39974
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
39975
|
+
dirName: rulesyncSkill.getDirName(),
|
|
39976
|
+
frontmatter: musecodeFrontmatter,
|
|
39977
|
+
body: rulesyncSkill.getBody(),
|
|
39978
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
39979
|
+
validate,
|
|
39980
|
+
global
|
|
39981
|
+
});
|
|
39982
|
+
}
|
|
39983
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
39984
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
39985
|
+
return targets.includes("*") || targets.includes("musecode");
|
|
39986
|
+
}
|
|
39987
|
+
static async fromDir(params) {
|
|
39988
|
+
const loaded = await this.loadSkillDirContent({
|
|
39989
|
+
...params,
|
|
39990
|
+
getSettablePaths: MusecodeSkill.getSettablePaths
|
|
39991
|
+
});
|
|
39992
|
+
const result = MusecodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
39993
|
+
if (!result.success) {
|
|
39994
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
39995
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
39996
|
+
}
|
|
39997
|
+
return new MusecodeSkill({
|
|
39998
|
+
outputRoot: loaded.outputRoot,
|
|
39999
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
40000
|
+
dirName: loaded.dirName,
|
|
40001
|
+
frontmatter: result.data,
|
|
40002
|
+
body: loaded.body,
|
|
40003
|
+
otherFiles: loaded.otherFiles,
|
|
40004
|
+
validate: true,
|
|
40005
|
+
global: loaded.global
|
|
40006
|
+
});
|
|
40007
|
+
}
|
|
40008
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
40009
|
+
const settablePaths = MusecodeSkill.getSettablePaths({ global });
|
|
40010
|
+
return new MusecodeSkill({
|
|
40011
|
+
outputRoot,
|
|
40012
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
40013
|
+
dirName,
|
|
40014
|
+
frontmatter: {
|
|
40015
|
+
name: "",
|
|
40016
|
+
description: ""
|
|
40017
|
+
},
|
|
40018
|
+
body: "",
|
|
40019
|
+
otherFiles: [],
|
|
40020
|
+
validate: false,
|
|
40021
|
+
global
|
|
40022
|
+
});
|
|
40023
|
+
}
|
|
40024
|
+
};
|
|
40025
|
+
//#endregion
|
|
38431
40026
|
//#region src/features/skills/opencode-skill.ts
|
|
38432
40027
|
const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
38433
40028
|
name: zod_mini.z.string(),
|
|
@@ -40256,6 +41851,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
40256
41851
|
supportsGlobal: true
|
|
40257
41852
|
}
|
|
40258
41853
|
}],
|
|
41854
|
+
["musecode", {
|
|
41855
|
+
class: MusecodeSkill,
|
|
41856
|
+
meta: {
|
|
41857
|
+
supportsProject: true,
|
|
41858
|
+
supportsSimulated: false,
|
|
41859
|
+
supportsGlobal: true
|
|
41860
|
+
}
|
|
41861
|
+
}],
|
|
40259
41862
|
["opencode", {
|
|
40260
41863
|
class: OpenCodeSkill,
|
|
40261
41864
|
meta: {
|
|
@@ -42776,7 +44379,7 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
|
|
|
42776
44379
|
* `sub_recipes` list, which rulesync never wrote) — so those files were inert.
|
|
42777
44380
|
* The custom-agent surface is the one Goose actually reads.
|
|
42778
44381
|
*
|
|
42779
|
-
* @see https://
|
|
44382
|
+
* @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
|
|
42780
44383
|
*/
|
|
42781
44384
|
const GooseSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
42782
44385
|
name: zod_mini.z.string(),
|
|
@@ -48110,7 +49713,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
48110
49713
|
* touches during a session. The separate `.goose/memories/` tree is the Memory
|
|
48111
49714
|
* extension's storage and is NOT auto-loaded as session context.
|
|
48112
49715
|
* (Verified against the official docs:
|
|
48113
|
-
* https://
|
|
49716
|
+
* https://goose-docs.ai/docs/guides/context-engineering/using-goosehints/)
|
|
48114
49717
|
*
|
|
48115
49718
|
* rulesync's topic-based non-root rules have no project subdirectory to map onto,
|
|
48116
49719
|
* so writing them under `.goose/memories/` made them effectively invisible to
|
|
@@ -48121,7 +49724,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
48121
49724
|
* Goose uses plain markdown files (.goosehints) without frontmatter.
|
|
48122
49725
|
*
|
|
48123
49726
|
* Global scope emits only `~/.config/goose/.goosehints`. Goose v1.41.0 (PR
|
|
48124
|
-
*
|
|
49727
|
+
* aaif-goose/goose#9736) additionally loads the vendor-neutral
|
|
48125
49728
|
* `~/.agents/AGENTS.md` alongside the config-dir hints, but rulesync
|
|
48126
49729
|
* deliberately does not emit that shared path from the goose target: the
|
|
48127
49730
|
* config-dir hints remain fully loaded (no capability loss), and the
|
|
@@ -48829,6 +50432,73 @@ var KiroIdeRule = class extends KiroRule {
|
|
|
48829
50432
|
}
|
|
48830
50433
|
};
|
|
48831
50434
|
//#endregion
|
|
50435
|
+
//#region src/features/rules/musecode-rule.ts
|
|
50436
|
+
var MusecodeRule = class MusecodeRule extends ToolRule {
|
|
50437
|
+
constructor({ fileContent, root, ...rest }) {
|
|
50438
|
+
super({
|
|
50439
|
+
...rest,
|
|
50440
|
+
fileContent,
|
|
50441
|
+
root: root ?? false
|
|
50442
|
+
});
|
|
50443
|
+
}
|
|
50444
|
+
static getSettablePaths(_options = {}) {
|
|
50445
|
+
return { root: {
|
|
50446
|
+
relativeDirPath: ".",
|
|
50447
|
+
relativeFilePath: MUSECODE_RULE_FILE_NAME
|
|
50448
|
+
} };
|
|
50449
|
+
}
|
|
50450
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
50451
|
+
const { root } = this.getSettablePaths({ global });
|
|
50452
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
50453
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
50454
|
+
return new MusecodeRule({
|
|
50455
|
+
outputRoot,
|
|
50456
|
+
relativeDirPath: root.relativeDirPath,
|
|
50457
|
+
relativeFilePath: root.relativeFilePath,
|
|
50458
|
+
fileContent,
|
|
50459
|
+
validate,
|
|
50460
|
+
root: true
|
|
50461
|
+
});
|
|
50462
|
+
}
|
|
50463
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
50464
|
+
const { root } = this.getSettablePaths({ global });
|
|
50465
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
50466
|
+
return new MusecodeRule({
|
|
50467
|
+
outputRoot,
|
|
50468
|
+
relativeDirPath: root.relativeDirPath,
|
|
50469
|
+
relativeFilePath: root.relativeFilePath,
|
|
50470
|
+
fileContent: rulesyncRule.getBody(),
|
|
50471
|
+
validate,
|
|
50472
|
+
root: isRoot
|
|
50473
|
+
});
|
|
50474
|
+
}
|
|
50475
|
+
toRulesyncRule() {
|
|
50476
|
+
return this.toRulesyncRuleDefault();
|
|
50477
|
+
}
|
|
50478
|
+
validate() {
|
|
50479
|
+
return {
|
|
50480
|
+
success: true,
|
|
50481
|
+
error: null
|
|
50482
|
+
};
|
|
50483
|
+
}
|
|
50484
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
50485
|
+
return new MusecodeRule({
|
|
50486
|
+
outputRoot,
|
|
50487
|
+
relativeDirPath,
|
|
50488
|
+
relativeFilePath,
|
|
50489
|
+
fileContent: "",
|
|
50490
|
+
validate: false,
|
|
50491
|
+
root: relativeFilePath === "AGENTS.md" && relativeDirPath === "."
|
|
50492
|
+
});
|
|
50493
|
+
}
|
|
50494
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
50495
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
50496
|
+
rulesyncRule,
|
|
50497
|
+
toolTarget: "musecode"
|
|
50498
|
+
});
|
|
50499
|
+
}
|
|
50500
|
+
};
|
|
50501
|
+
//#endregion
|
|
48832
50502
|
//#region src/features/rules/opencode-rule.ts
|
|
48833
50503
|
var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
48834
50504
|
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
@@ -50409,6 +52079,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
50409
52079
|
ruleDiscoveryMode: "toon"
|
|
50410
52080
|
}
|
|
50411
52081
|
}],
|
|
52082
|
+
["musecode", {
|
|
52083
|
+
class: MusecodeRule,
|
|
52084
|
+
meta: {
|
|
52085
|
+
extension: "md",
|
|
52086
|
+
supportsGlobal: false,
|
|
52087
|
+
ruleDiscoveryMode: "auto",
|
|
52088
|
+
collisionPolicy: "fold"
|
|
52089
|
+
}
|
|
52090
|
+
}],
|
|
50412
52091
|
["opencode", {
|
|
50413
52092
|
class: OpenCodeRule,
|
|
50414
52093
|
meta: {
|