rulesync 16.8.0 → 16.9.1
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 +2 -2
- package/dist/cli/index.cjs +76 -6
- package/dist/cli/index.js +76 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-DEK1TMmW.cjs → import-BaRDaZET.cjs} +1598 -433
- package/dist/{import-KXnvmbzr.js → import-BpKoN2US.js} +1600 -435
- package/dist/import-BpKoN2US.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +14 -14
- package/dist/import-KXnvmbzr.js.map +0 -1
|
@@ -382,6 +382,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
382
382
|
];
|
|
383
383
|
const hooksProcessorToolTargetTuple = [
|
|
384
384
|
"amp",
|
|
385
|
+
"cline",
|
|
385
386
|
"antigravity-cli",
|
|
386
387
|
"antigravity-ide",
|
|
387
388
|
"antigravity-plugin",
|
|
@@ -657,6 +658,29 @@ async function writeFileContent(filepath, content) {
|
|
|
657
658
|
await ensureDir((0, node_path.dirname)(filepath));
|
|
658
659
|
await (0, node_fs_promises.writeFile)(filepath, content, "utf-8");
|
|
659
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Apply a POSIX mode to an existing file. Windows has no executable bit and
|
|
663
|
+
* `chmod` there only toggles the read-only flag, so the call is skipped rather
|
|
664
|
+
* than writing a mode the platform cannot honor.
|
|
665
|
+
*/
|
|
666
|
+
async function applyFileMode(filepath, mode) {
|
|
667
|
+
if (process.platform === "win32") return;
|
|
668
|
+
await (0, node_fs_promises.chmod)(filepath, mode);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Restore an executable bit that went missing (interrupted run, a copy that
|
|
672
|
+
* dropped the mode). A file whose mode is merely stricter than `mode` — the
|
|
673
|
+
* user chose 0700 over 0755 — is left alone.
|
|
674
|
+
*/
|
|
675
|
+
async function restoreMissingExecutableBit(filepath, mode) {
|
|
676
|
+
if (process.platform === "win32") return;
|
|
677
|
+
try {
|
|
678
|
+
if (((await (0, node_fs_promises.stat)(filepath)).mode & 73) !== 0) return;
|
|
679
|
+
} catch {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
await (0, node_fs_promises.chmod)(filepath, mode);
|
|
683
|
+
}
|
|
660
684
|
async function writeFileBuffer(filepath, buffer) {
|
|
661
685
|
await ensureDir((0, node_path.dirname)(filepath));
|
|
662
686
|
await (0, node_fs_promises.writeFile)(filepath, buffer);
|
|
@@ -1826,6 +1850,13 @@ var AiFile = class {
|
|
|
1826
1850
|
return false;
|
|
1827
1851
|
}
|
|
1828
1852
|
/**
|
|
1853
|
+
* POSIX mode to apply after writing, or `undefined` to leave the default
|
|
1854
|
+
* alone. Override in subclasses whose output the tool executes directly
|
|
1855
|
+
* (e.g. Cline's hook scripts, which are spawned by path and therefore need
|
|
1856
|
+
* the executable bit).
|
|
1857
|
+
*/
|
|
1858
|
+
getFileMode() {}
|
|
1859
|
+
/**
|
|
1829
1860
|
* Returns whether this file can be deleted by rulesync.
|
|
1830
1861
|
* Override in subclasses that should not be deleted (e.g., user-managed config files).
|
|
1831
1862
|
*/
|
|
@@ -2269,7 +2300,8 @@ const HOOK_EVENTS = [
|
|
|
2269
2300
|
"fileChanged",
|
|
2270
2301
|
"directoryAdded",
|
|
2271
2302
|
"elicitation",
|
|
2272
|
-
"elicitationResult"
|
|
2303
|
+
"elicitationResult",
|
|
2304
|
+
"sessionDelete"
|
|
2273
2305
|
];
|
|
2274
2306
|
/** Hook events supported by Cursor. */
|
|
2275
2307
|
const CURSOR_HOOK_EVENTS = [
|
|
@@ -2365,9 +2397,10 @@ const DEVIN_HOOK_EVENTS = [
|
|
|
2365
2397
|
/**
|
|
2366
2398
|
* Hook events supported by OpenCode.
|
|
2367
2399
|
*
|
|
2368
|
-
* `preCompact` maps to `experimental.session.compacting
|
|
2369
|
-
*
|
|
2370
|
-
*
|
|
2400
|
+
* `preCompact` maps to `experimental.session.compacting` and
|
|
2401
|
+
* `beforeSubmitPrompt` to `chat.message`, both of which the plugin docs
|
|
2402
|
+
* document as named `(input, output)` hooks rather than `event.type`
|
|
2403
|
+
* dispatches; the other entries are all generic events.
|
|
2371
2404
|
*
|
|
2372
2405
|
* @see https://opencode.ai/docs/plugins/
|
|
2373
2406
|
*/
|
|
@@ -2383,16 +2416,22 @@ const OPENCODE_HOOK_EVENTS = [
|
|
|
2383
2416
|
"preCompact",
|
|
2384
2417
|
"postCompact",
|
|
2385
2418
|
"afterError",
|
|
2386
|
-
"fileChanged"
|
|
2419
|
+
"fileChanged",
|
|
2420
|
+
"notification",
|
|
2421
|
+
"permissionDenied",
|
|
2422
|
+
"beforeSubmitPrompt"
|
|
2387
2423
|
];
|
|
2388
2424
|
/**
|
|
2389
|
-
* Hook events supported by Kilo.
|
|
2390
|
-
*
|
|
2391
|
-
* `file.watcher.updated` and the
|
|
2425
|
+
* Hook events supported by Kilo. Kilo's plugin docs list the same event surface
|
|
2426
|
+
* as OpenCode's — including `session.compacted`, `session.error`,
|
|
2427
|
+
* `file.watcher.updated`, `permission.replied`, `chat.message` and the
|
|
2428
|
+
* experimental compaction hook — with one exception: they document no TUI
|
|
2429
|
+
* events at all, so `tui.toast.show` (canonical `notification`) is left out
|
|
2430
|
+
* rather than emitted into a plugin where it may never fire.
|
|
2392
2431
|
*
|
|
2393
2432
|
* @see https://kilo.ai/docs/automate/extending/plugins
|
|
2394
2433
|
*/
|
|
2395
|
-
const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS;
|
|
2434
|
+
const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS.filter((event) => event !== "notification");
|
|
2396
2435
|
/**
|
|
2397
2436
|
* Hook events supported by Pi Coding Agent, bridged through a generated
|
|
2398
2437
|
* TypeScript extension (Pi has no static hook config file; its extension API
|
|
@@ -2430,6 +2469,29 @@ const AMP_HOOK_EVENTS = [
|
|
|
2430
2469
|
"stop"
|
|
2431
2470
|
];
|
|
2432
2471
|
/**
|
|
2472
|
+
* Hook events supported by Cline's file-based hooks. Cline resolves one
|
|
2473
|
+
* executable per lifecycle event from its hooks directory, and the event names
|
|
2474
|
+
* it accepts are fixed by `VALID_HOOK_TYPES` in
|
|
2475
|
+
* `apps/vscode/src/core/hooks/utils.ts`: `TaskStart`, `TaskResume`,
|
|
2476
|
+
* `TaskCancel`, `TaskComplete`, `PreToolUse`, `PostToolUse`,
|
|
2477
|
+
* `UserPromptSubmit`, `Notification` and `PreCompact`.
|
|
2478
|
+
*
|
|
2479
|
+
* `TaskResume` and `TaskCancel` have no canonical counterpart and stay
|
|
2480
|
+
* unmapped rather than being approximated by `sessionEnd` / `stop`, whose
|
|
2481
|
+
* semantics differ.
|
|
2482
|
+
*
|
|
2483
|
+
* @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
|
|
2484
|
+
*/
|
|
2485
|
+
const CLINE_HOOK_EVENTS = [
|
|
2486
|
+
"sessionStart",
|
|
2487
|
+
"preToolUse",
|
|
2488
|
+
"postToolUse",
|
|
2489
|
+
"beforeSubmitPrompt",
|
|
2490
|
+
"preCompact",
|
|
2491
|
+
"notification",
|
|
2492
|
+
"taskCompleted"
|
|
2493
|
+
];
|
|
2494
|
+
/**
|
|
2433
2495
|
* Hook events supported by GitHub Copilot (cloud coding agent).
|
|
2434
2496
|
*
|
|
2435
2497
|
* GitHub now documents an eight-event surface for `.github/hooks/*.json`:
|
|
@@ -2567,7 +2629,11 @@ const GOOSE_HOOK_EVENTS = [
|
|
|
2567
2629
|
"beforeShellExecution",
|
|
2568
2630
|
"afterShellExecution"
|
|
2569
2631
|
];
|
|
2570
|
-
/**
|
|
2632
|
+
/**
|
|
2633
|
+
* Hook events supported by the embedded agent-config hook format, which only
|
|
2634
|
+
* the deprecated `kiro` alias still writes. See {@link KIRO_IDE_HOOK_EVENTS}
|
|
2635
|
+
* for the standalone format both Kiro products read today.
|
|
2636
|
+
*/
|
|
2571
2637
|
const KIRO_HOOK_EVENTS = [
|
|
2572
2638
|
"sessionStart",
|
|
2573
2639
|
"sessionEnd",
|
|
@@ -2577,15 +2643,17 @@ const KIRO_HOOK_EVENTS = [
|
|
|
2577
2643
|
"stop"
|
|
2578
2644
|
];
|
|
2579
2645
|
/**
|
|
2580
|
-
* Hook events supported by
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
2587
|
-
*
|
|
2588
|
-
*
|
|
2646
|
+
* Hook events supported by Kiro's standalone hooks format
|
|
2647
|
+
* (`.kiro/hooks/*.json` v1), which the Kiro IDE and Kiro CLI 3.0 both read.
|
|
2648
|
+
*
|
|
2649
|
+
* Kiro exposes PascalCase triggers. rulesync maps the canonical lifecycle
|
|
2650
|
+
* events that have a clean 1:1 equivalent: `SessionStart`, `Stop`,
|
|
2651
|
+
* `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
|
|
2652
|
+
* file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
|
|
2653
|
+
* (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
|
|
2654
|
+
* those can still be emitted verbatim via a `kiro-ide` or `kiro-cli` override
|
|
2655
|
+
* block (unknown event keys pass through unchanged). There is no `SessionEnd`
|
|
2656
|
+
* trigger, so the canonical `sessionEnd` has no home here.
|
|
2589
2657
|
* @see https://kiro.dev/docs/hooks/types/
|
|
2590
2658
|
*/
|
|
2591
2659
|
const KIRO_IDE_HOOK_EVENTS = [
|
|
@@ -2707,7 +2775,8 @@ const QWENCODE_HOOK_EVENTS = [
|
|
|
2707
2775
|
"instructionsLoaded",
|
|
2708
2776
|
"todoCreated",
|
|
2709
2777
|
"todoCompleted",
|
|
2710
|
-
"messageDisplay"
|
|
2778
|
+
"messageDisplay",
|
|
2779
|
+
"sessionDelete"
|
|
2711
2780
|
];
|
|
2712
2781
|
/**
|
|
2713
2782
|
* Hook events supported by Reasonix.
|
|
@@ -2770,7 +2839,11 @@ const GROKCLI_HOOK_EVENTS = [
|
|
|
2770
2839
|
/**
|
|
2771
2840
|
* Hook events supported by Kimi Code.
|
|
2772
2841
|
*
|
|
2773
|
-
* Kimi Code also exposes `Interrupt`,
|
|
2842
|
+
* Kimi Code also exposes `PermissionResult`, `Interrupt`, and the four events
|
|
2843
|
+
* added in 0.32.0 (`TurnStarted`, `UserPromptQueued`, `TaskStarted`,
|
|
2844
|
+
* `SessionHeartbeat`), none of which have a canonical rulesync event. They are
|
|
2845
|
+
* listed in `KIMI_CODE_NATIVE_HOOK_EVENTS` so a per-tool `kimi-code` override
|
|
2846
|
+
* can address them by their native name.
|
|
2774
2847
|
*
|
|
2775
2848
|
* @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
|
|
2776
2849
|
*/
|
|
@@ -2806,10 +2879,27 @@ const CANONICAL_TO_KIMI_CODE_EVENT_NAMES = {
|
|
|
2806
2879
|
preCompact: "PreCompact",
|
|
2807
2880
|
postCompact: "PostCompact"
|
|
2808
2881
|
};
|
|
2882
|
+
/**
|
|
2883
|
+
* Every event name Kimi Code accepts in a `[[hooks]]` entry: the ones with a
|
|
2884
|
+
* canonical rulesync counterpart plus the native-only ones, which are reachable
|
|
2885
|
+
* through a per-tool `kimi-code` override that names them directly.
|
|
2886
|
+
*
|
|
2887
|
+
* `TurnStarted`, `UserPromptQueued`, `TaskStarted`, and `SessionHeartbeat` were
|
|
2888
|
+
* added in Kimi Code 0.32.0. They stay native-only: `TaskStarted` fires when a
|
|
2889
|
+
* background task starts and matches on task kind, whereas the canonical
|
|
2890
|
+
* `taskCreated` models Claude Code's blocking, matcher-less `TaskCreated`
|
|
2891
|
+
* (fired while a task is being created), so the two are not interchangeable.
|
|
2892
|
+
*
|
|
2893
|
+
* @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
|
|
2894
|
+
*/
|
|
2809
2895
|
const KIMI_CODE_NATIVE_HOOK_EVENTS = [
|
|
2810
2896
|
...Object.values(CANONICAL_TO_KIMI_CODE_EVENT_NAMES),
|
|
2811
2897
|
"PermissionResult",
|
|
2812
|
-
"Interrupt"
|
|
2898
|
+
"Interrupt",
|
|
2899
|
+
"TurnStarted",
|
|
2900
|
+
"UserPromptQueued",
|
|
2901
|
+
"TaskStarted",
|
|
2902
|
+
"SessionHeartbeat"
|
|
2813
2903
|
];
|
|
2814
2904
|
const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIMI_CODE_EVENT_NAMES).map(([canonical, kimiCode]) => [kimiCode, canonical]));
|
|
2815
2905
|
/**
|
|
@@ -3086,14 +3176,20 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
|
|
|
3086
3176
|
stop: "session.idle",
|
|
3087
3177
|
afterFileEdit: "file.edited",
|
|
3088
3178
|
permissionRequest: "permission.asked",
|
|
3179
|
+
permissionDenied: "permission.replied",
|
|
3180
|
+
notification: "tui.toast.show",
|
|
3089
3181
|
preCompact: "experimental.session.compacting",
|
|
3182
|
+
beforeSubmitPrompt: "chat.message",
|
|
3090
3183
|
postCompact: "session.compacted",
|
|
3091
3184
|
afterError: "session.error",
|
|
3092
3185
|
fileChanged: "file.watcher.updated"
|
|
3093
3186
|
};
|
|
3094
3187
|
/**
|
|
3095
3188
|
* Map canonical camelCase event names to Kilo dot-notation.
|
|
3096
|
-
*
|
|
3189
|
+
*
|
|
3190
|
+
* Shared with OpenCode: the two name the same events. The `notification` entry
|
|
3191
|
+
* is unreachable for Kilo because `KILO_HOOK_EVENTS` omits it, and the
|
|
3192
|
+
* generator emits only supported events.
|
|
3097
3193
|
*/
|
|
3098
3194
|
const CANONICAL_TO_KILO_EVENT_NAMES = CANONICAL_TO_OPENCODE_EVENT_NAMES;
|
|
3099
3195
|
/**
|
|
@@ -3131,6 +3227,16 @@ const CANONICAL_TO_AMP_EVENT_NAMES = {
|
|
|
3131
3227
|
beforeSubmitPrompt: "agent.start",
|
|
3132
3228
|
stop: "agent.end"
|
|
3133
3229
|
};
|
|
3230
|
+
/** Map canonical hook events to Cline's `VALID_HOOK_TYPES` file names. */
|
|
3231
|
+
const CANONICAL_TO_CLINE_EVENT_NAMES = {
|
|
3232
|
+
sessionStart: "TaskStart",
|
|
3233
|
+
preToolUse: "PreToolUse",
|
|
3234
|
+
postToolUse: "PostToolUse",
|
|
3235
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
3236
|
+
preCompact: "PreCompact",
|
|
3237
|
+
notification: "Notification",
|
|
3238
|
+
taskCompleted: "TaskComplete"
|
|
3239
|
+
};
|
|
3134
3240
|
/**
|
|
3135
3241
|
* Map canonical camelCase event names to Copilot camelCase.
|
|
3136
3242
|
*/
|
|
@@ -3367,7 +3473,8 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
|
|
|
3367
3473
|
instructionsLoaded: "InstructionsLoaded",
|
|
3368
3474
|
todoCreated: "TodoCreated",
|
|
3369
3475
|
todoCompleted: "TodoCompleted",
|
|
3370
|
-
messageDisplay: "MessageDisplay"
|
|
3476
|
+
messageDisplay: "MessageDisplay",
|
|
3477
|
+
sessionDelete: "SessionDelete"
|
|
3371
3478
|
};
|
|
3372
3479
|
/**
|
|
3373
3480
|
* Map Qwen Code PascalCase event names to canonical camelCase.
|
|
@@ -4204,14 +4311,20 @@ const VibePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4204
4311
|
* Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
|
|
4205
4312
|
* autonomy settings with no canonical permission category — `approvalMode`
|
|
4206
4313
|
* (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
|
|
4207
|
-
* (`mode`/`networkAccess`). Fields placed here are merged into the top
|
|
4208
|
-
*
|
|
4209
|
-
*
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
4314
|
+
* (`mode`/`networkAccess`). Fields placed here are merged into the top level of
|
|
4315
|
+
* `~/.cursor/cli-config.json` and emitted only for Cursor, while the shared
|
|
4316
|
+
* `permission` block continues to drive the `permissions.allow`/`permissions.deny`
|
|
4317
|
+
* arrays. Kept a `looseObject` so extra config keys can be authored (they are
|
|
4318
|
+
* merged verbatim on generate); `sandbox`'s accepted values are not documented
|
|
4319
|
+
* so it passes through verbatim.
|
|
4320
|
+
*
|
|
4321
|
+
* These are **global-only** settings: Cursor documents that "Only permissions
|
|
4322
|
+
* can be configured at the project level. All other CLI settings must be set
|
|
4323
|
+
* globally", so a project generate skips them with a warning instead of writing
|
|
4324
|
+
* keys `.cursor/cli.json` would ignore. Author them with `--global`.
|
|
4325
|
+
*
|
|
4213
4326
|
* Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
|
|
4214
|
-
* authored here reach
|
|
4327
|
+
* authored here reach the global config on generate but are not re-extracted.
|
|
4215
4328
|
*
|
|
4216
4329
|
* @example
|
|
4217
4330
|
* { "approvalMode": "auto-review" }
|
|
@@ -4229,7 +4342,10 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4229
4342
|
* Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
|
|
4230
4343
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
4231
4344
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
4232
|
-
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust
|
|
4345
|
+
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`,
|
|
4346
|
+
* `allowedHttpHookUrls`, `allowPrivateNetworkHooks` — the latter is honored by
|
|
4347
|
+
* Qwen Code only in user/system settings, so generate skips it in project scope).
|
|
4348
|
+
* It also
|
|
4233
4349
|
* exposes `permissions.autoMode` (the Auto Mode classifier config:
|
|
4234
4350
|
* `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` — see
|
|
4235
4351
|
* https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
|
|
@@ -4995,7 +5111,10 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
4995
5111
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
4996
5112
|
description: zod_mini.z.optional(zod_mini.z.string())
|
|
4997
5113
|
})),
|
|
4998
|
-
pi: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5114
|
+
pi: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5115
|
+
systemPrompt: zod_mini.z.optional(zod_mini.z.enum(["append"])),
|
|
5116
|
+
contextFile: zod_mini.z.optional(zod_mini.z.enum(["override"]))
|
|
5117
|
+
})),
|
|
4999
5118
|
takt: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5000
5119
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
5001
5120
|
extends: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -5190,7 +5309,10 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5190
5309
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5191
5310
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5192
5311
|
"scheduled-task": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5193
|
-
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
5312
|
+
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
5313
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
5314
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
5315
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
5194
5316
|
})),
|
|
5195
5317
|
codexcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5196
5318
|
"short-description": zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -5802,13 +5924,20 @@ var FeatureProcessor = class {
|
|
|
5802
5924
|
filePath,
|
|
5803
5925
|
content: contentWithNewline
|
|
5804
5926
|
})) continue;
|
|
5927
|
+
const fileMode = aiFile.getFileMode?.();
|
|
5805
5928
|
if (fileContentsEquivalent({
|
|
5806
5929
|
filePath,
|
|
5807
5930
|
expected: contentWithNewline,
|
|
5808
5931
|
existing: existingContent
|
|
5809
|
-
}))
|
|
5932
|
+
})) {
|
|
5933
|
+
if (fileMode !== void 0 && !this.dryRun) await restoreMissingExecutableBit(filePath, fileMode);
|
|
5934
|
+
continue;
|
|
5935
|
+
}
|
|
5810
5936
|
if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${filePath}`);
|
|
5811
|
-
else
|
|
5937
|
+
else {
|
|
5938
|
+
await writeFileContent(filePath, contentWithNewline);
|
|
5939
|
+
if (fileMode !== void 0) await applyFileMode(filePath, fileMode);
|
|
5940
|
+
}
|
|
5812
5941
|
changedCount++;
|
|
5813
5942
|
changedPaths.push(aiFile.getRelativePathFromCwd());
|
|
5814
5943
|
}
|
|
@@ -7169,10 +7298,16 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7169
7298
|
".config/goose/config.yaml": {
|
|
7170
7299
|
format: "yaml",
|
|
7171
7300
|
invalidRootPolicy: "error",
|
|
7172
|
-
features: {
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7301
|
+
features: {
|
|
7302
|
+
mcp: {
|
|
7303
|
+
kind: "replace-owned-keys",
|
|
7304
|
+
ownedKeys: ["extensions"]
|
|
7305
|
+
},
|
|
7306
|
+
commands: {
|
|
7307
|
+
kind: "replace-owned-keys",
|
|
7308
|
+
ownedKeys: ["slash_commands"]
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7176
7311
|
},
|
|
7177
7312
|
[CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
|
|
7178
7313
|
format: "toml",
|
|
@@ -7876,6 +8011,22 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
7876
8011
|
}
|
|
7877
8012
|
};
|
|
7878
8013
|
//#endregion
|
|
8014
|
+
//#region src/constants/goose-paths.ts
|
|
8015
|
+
const GOOSE_DIR = ".goose";
|
|
8016
|
+
const GOOSE_GLOBAL_DIR = (0, node_path.join)(".config", "goose");
|
|
8017
|
+
const GOOSE_RULE_FILE_NAME = ".goosehints";
|
|
8018
|
+
const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
8019
|
+
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
8020
|
+
const GOOSE_HOOKS_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync", "hooks");
|
|
8021
|
+
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
8022
|
+
const GOOSE_PLUGIN_MCP_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync");
|
|
8023
|
+
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
8024
|
+
const GOOSE_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "skills");
|
|
8025
|
+
const GOOSE_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "recipes");
|
|
8026
|
+
const GOOSE_GLOBAL_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "recipes");
|
|
8027
|
+
const GOOSE_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "agents");
|
|
8028
|
+
const GOOSE_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "agents");
|
|
8029
|
+
//#endregion
|
|
7879
8030
|
//#region src/utils/tool-home.ts
|
|
7880
8031
|
/**
|
|
7881
8032
|
* Where the rulesync-side source files of a tool with a home override belong.
|
|
@@ -8817,6 +8968,9 @@ const CLINE_MCP_DIR_PATH = (0, node_path.join)(CLINE_DIR, "data", "settings");
|
|
|
8817
8968
|
const CLINE_MCP_FILE_NAME = "cline_mcp_settings.json";
|
|
8818
8969
|
const CLINE_PERMISSIONS_FILE_NAME = "command-permissions.json";
|
|
8819
8970
|
const CLINE_IGNORE_FILE_NAME = ".clineignore";
|
|
8971
|
+
const CLINE_HOOKS_DIR_PATH = (0, node_path.join)(CLINERULES_DIR, "hooks");
|
|
8972
|
+
const CLINE_HOOKS_GLOBAL_DIR_PATH = (0, node_path.join)("Documents", "Cline", "Hooks");
|
|
8973
|
+
const CLINE_HOOKS_MANIFEST_FILE_NAME = "rulesync-hooks.json";
|
|
8820
8974
|
//#endregion
|
|
8821
8975
|
//#region src/features/commands/cline-command.ts
|
|
8822
8976
|
var ClineCommand = class ClineCommand extends ToolCommand {
|
|
@@ -9278,6 +9432,8 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
|
9278
9432
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
9279
9433
|
const DEVIN_IGNORE_FILE_NAME = ".devinignore";
|
|
9280
9434
|
const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
|
|
9435
|
+
const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
|
|
9436
|
+
const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
|
|
9281
9437
|
//#endregion
|
|
9282
9438
|
//#region src/features/commands/command-skill-ownership.ts
|
|
9283
9439
|
/**
|
|
@@ -9526,24 +9682,99 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9526
9682
|
}
|
|
9527
9683
|
};
|
|
9528
9684
|
//#endregion
|
|
9529
|
-
//#region src/constants/goose-paths.ts
|
|
9530
|
-
const GOOSE_DIR = ".goose";
|
|
9531
|
-
const GOOSE_GLOBAL_DIR = (0, node_path.join)(".config", "goose");
|
|
9532
|
-
const GOOSE_RULE_FILE_NAME = ".goosehints";
|
|
9533
|
-
const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
9534
|
-
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
9535
|
-
const GOOSE_HOOKS_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync", "hooks");
|
|
9536
|
-
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
9537
|
-
const GOOSE_PLUGIN_MCP_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync");
|
|
9538
|
-
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
9539
|
-
const GOOSE_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "skills");
|
|
9540
|
-
const GOOSE_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "recipes");
|
|
9541
|
-
const GOOSE_GLOBAL_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "recipes");
|
|
9542
|
-
const GOOSE_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "agents");
|
|
9543
|
-
const GOOSE_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "agents");
|
|
9544
|
-
//#endregion
|
|
9545
9685
|
//#region src/features/commands/goose-command.ts
|
|
9546
9686
|
const RECIPE_VERSION = "1.0.0";
|
|
9687
|
+
const SLASH_COMMANDS_KEY = "slash_commands";
|
|
9688
|
+
const GOOSE_GLOBAL_RECIPES_POSIX_DIR = toPosixPath(GOOSE_GLOBAL_RECIPES_DIR_PATH);
|
|
9689
|
+
function slashCommandEntry({ outputRoot, relativeFilePath }) {
|
|
9690
|
+
const fileName = (0, node_path.basename)(toPosixPath(relativeFilePath));
|
|
9691
|
+
return {
|
|
9692
|
+
command: fileName.replace(/\.ya?ml$/, "").toLowerCase(),
|
|
9693
|
+
recipe_path: (0, node_path.join)(outputRoot, GOOSE_GLOBAL_RECIPES_DIR_PATH, fileName)
|
|
9694
|
+
};
|
|
9695
|
+
}
|
|
9696
|
+
/**
|
|
9697
|
+
* Whether an existing `slash_commands` entry points at a recipe rulesync owns:
|
|
9698
|
+
* a direct child of the global recipes directory. Anything else — a recipe
|
|
9699
|
+
* elsewhere on disk, or a sub-recipe under `recipes/subagents/` — belongs to
|
|
9700
|
+
* the user and is carried over untouched. A path that cannot be resolved into
|
|
9701
|
+
* the managed directory is preserved rather than claimed.
|
|
9702
|
+
*/
|
|
9703
|
+
function isManagedRecipePath(value) {
|
|
9704
|
+
if (typeof value !== "string") return false;
|
|
9705
|
+
const segments = toPosixPath(value).replace(/^~\//, "").split("/");
|
|
9706
|
+
const fileName = segments.pop();
|
|
9707
|
+
if (fileName === void 0 || fileName === "") return false;
|
|
9708
|
+
const dirSegments = GOOSE_GLOBAL_RECIPES_POSIX_DIR.split("/");
|
|
9709
|
+
return segments.length >= dirSegments.length && segments.slice(-dirSegments.length).join("/") === GOOSE_GLOBAL_RECIPES_POSIX_DIR;
|
|
9710
|
+
}
|
|
9711
|
+
/**
|
|
9712
|
+
* Whether a config file carries a registration rulesync owns. Rewriting a
|
|
9713
|
+
* config that holds none of them would reformat the user's file (comments and
|
|
9714
|
+
* all) for nothing, so both writers check this first.
|
|
9715
|
+
*/
|
|
9716
|
+
function hasManagedGooseSlashCommands(fileContent) {
|
|
9717
|
+
const existing = parseSharedConfig({
|
|
9718
|
+
format: "yaml",
|
|
9719
|
+
fileContent
|
|
9720
|
+
})[SLASH_COMMANDS_KEY];
|
|
9721
|
+
return Array.isArray(existing) && existing.some((entry) => isRecord$1(entry) && isManagedRecipePath(entry.recipe_path));
|
|
9722
|
+
}
|
|
9723
|
+
/**
|
|
9724
|
+
* Recompute `slash_commands` from the entries rulesync generates: user entries
|
|
9725
|
+
* pointing outside the managed recipes directory are carried over, entries
|
|
9726
|
+
* inside it are replaced (so a deleted command's registration is retracted),
|
|
9727
|
+
* and the key is dropped entirely when nothing is left.
|
|
9728
|
+
*/
|
|
9729
|
+
function getGooseSlashCommandsConfigContent({ currentContent, entries }) {
|
|
9730
|
+
const config = parseSharedConfig({
|
|
9731
|
+
format: "yaml",
|
|
9732
|
+
fileContent: currentContent
|
|
9733
|
+
});
|
|
9734
|
+
const next = [...(Array.isArray(config[SLASH_COMMANDS_KEY]) ? config[SLASH_COMMANDS_KEY] : []).filter((entry) => !isRecord$1(entry) || !isManagedRecipePath(entry.recipe_path)), ...entries];
|
|
9735
|
+
return applySharedConfigPatch({
|
|
9736
|
+
fileKey: sharedConfigFileKey({
|
|
9737
|
+
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
9738
|
+
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
9739
|
+
}),
|
|
9740
|
+
feature: "commands",
|
|
9741
|
+
existingContent: currentContent,
|
|
9742
|
+
patch: { [SLASH_COMMANDS_KEY]: next.length > 0 ? next : void 0 }
|
|
9743
|
+
});
|
|
9744
|
+
}
|
|
9745
|
+
/**
|
|
9746
|
+
* The Goose user `config.yaml`, carrying the `slash_commands` registrations for
|
|
9747
|
+
* the generated recipes. The file is shared with the user's own settings, so it
|
|
9748
|
+
* is always merged into rather than replaced.
|
|
9749
|
+
*/
|
|
9750
|
+
var GooseCommandConfigFile = class extends ToolFile {
|
|
9751
|
+
entries;
|
|
9752
|
+
constructor(params) {
|
|
9753
|
+
super(params);
|
|
9754
|
+
this.entries = params.entries;
|
|
9755
|
+
}
|
|
9756
|
+
validate() {
|
|
9757
|
+
return {
|
|
9758
|
+
success: true,
|
|
9759
|
+
error: null
|
|
9760
|
+
};
|
|
9761
|
+
}
|
|
9762
|
+
shouldMergeExistingFileContent() {
|
|
9763
|
+
return true;
|
|
9764
|
+
}
|
|
9765
|
+
setFileContent(newFileContent) {
|
|
9766
|
+
super.setFileContent(getGooseSlashCommandsConfigContent({
|
|
9767
|
+
currentContent: newFileContent,
|
|
9768
|
+
entries: this.entries
|
|
9769
|
+
}));
|
|
9770
|
+
}
|
|
9771
|
+
getFileContent() {
|
|
9772
|
+
return getGooseSlashCommandsConfigContent({
|
|
9773
|
+
currentContent: super.getFileContent(),
|
|
9774
|
+
entries: this.entries
|
|
9775
|
+
});
|
|
9776
|
+
}
|
|
9777
|
+
};
|
|
9547
9778
|
/**
|
|
9548
9779
|
* Goose recipe files are reusable YAML workflow documents. A recipe requires
|
|
9549
9780
|
* `version`, `title`, and `description`, plus at least one of `instructions` /
|
|
@@ -9578,6 +9809,40 @@ var GooseCommand = class GooseCommand extends ToolCommand {
|
|
|
9578
9809
|
static getSettablePaths({ global = false } = {}) {
|
|
9579
9810
|
return { relativeDirPath: global ? GOOSE_GLOBAL_RECIPES_DIR_PATH : GOOSE_RECIPES_DIR_PATH };
|
|
9580
9811
|
}
|
|
9812
|
+
/**
|
|
9813
|
+
* The user `config.yaml` holding the `slash_commands` registrations. Global
|
|
9814
|
+
* scope only — Goose has no project-level registration surface.
|
|
9815
|
+
*/
|
|
9816
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
9817
|
+
if (!global) return [];
|
|
9818
|
+
return [{
|
|
9819
|
+
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
9820
|
+
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
9821
|
+
}];
|
|
9822
|
+
}
|
|
9823
|
+
/**
|
|
9824
|
+
* Register the generated recipes as slash commands. The config file is also
|
|
9825
|
+
* emitted when no command is generated but the existing file still carries
|
|
9826
|
+
* managed registrations, so removing the last command retracts them instead of
|
|
9827
|
+
* leaving `/name` pointing at a deleted recipe.
|
|
9828
|
+
*/
|
|
9829
|
+
static async getAuxiliaryFiles({ toolCommands, outputRoot = process.cwd(), global = false, forDeletion = false }) {
|
|
9830
|
+
if (!global || forDeletion) return [];
|
|
9831
|
+
const entries = toolCommands.map((command) => slashCommandEntry({
|
|
9832
|
+
outputRoot,
|
|
9833
|
+
relativeFilePath: command.getRelativeFilePath()
|
|
9834
|
+
}));
|
|
9835
|
+
const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME));
|
|
9836
|
+
if (entries.length === 0 && !hasManagedGooseSlashCommands(existingContent ?? "")) return [];
|
|
9837
|
+
return [new GooseCommandConfigFile({
|
|
9838
|
+
outputRoot,
|
|
9839
|
+
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
9840
|
+
relativeFilePath: GOOSE_MCP_FILE_NAME,
|
|
9841
|
+
fileContent: existingContent ?? "",
|
|
9842
|
+
entries,
|
|
9843
|
+
global
|
|
9844
|
+
})];
|
|
9845
|
+
}
|
|
9581
9846
|
parseRecipeContent(content) {
|
|
9582
9847
|
const where = (0, node_path.join)(this.relativeDirPath, this.relativeFilePath);
|
|
9583
9848
|
let parsed;
|
|
@@ -11063,8 +11328,9 @@ const KIRO_SETTINGS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "settings");
|
|
|
11063
11328
|
const KIRO_AGENTS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "agents");
|
|
11064
11329
|
const KIRO_HOOKS_FILE_NAME = "default.json";
|
|
11065
11330
|
/**
|
|
11066
|
-
* Kiro
|
|
11067
|
-
*
|
|
11331
|
+
* Kiro stores hooks as structured JSON files in `.kiro/hooks/` (workspace) and
|
|
11332
|
+
* `~/.kiro/hooks/` (user) — the format the IDE reads and the one Kiro CLI 3.0
|
|
11333
|
+
* migrated to. A single file may declare multiple
|
|
11068
11334
|
* hooks in its `hooks` array, so rulesync emits all generated hooks into one
|
|
11069
11335
|
* `rulesync.json` file per scope.
|
|
11070
11336
|
* @see https://kiro.dev/docs/hooks/
|
|
@@ -11407,6 +11673,7 @@ const PI_EXTENSIONS_DIR_PATH = (0, node_path.join)(".pi", "extensions");
|
|
|
11407
11673
|
const PI_PROMPTS_DIR_PATH = (0, node_path.join)(".pi", "prompts");
|
|
11408
11674
|
const PI_SKILLS_DIR_PATH = (0, node_path.join)(".pi", "skills");
|
|
11409
11675
|
const PI_RULE_FILE_NAME = "AGENTS.md";
|
|
11676
|
+
const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
|
|
11410
11677
|
const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
|
|
11411
11678
|
const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
|
|
11412
11679
|
//#endregion
|
|
@@ -12898,6 +13165,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
12898
13165
|
}));
|
|
12899
13166
|
const shouldDisableHermesCommandsPlugin = this.toolTarget === "hermesagent" && existingFiles.some((file) => file.getFilePath() === ownershipPath) && !generatedFiles.some((file) => file.getFilePath() === ownershipPath);
|
|
12900
13167
|
let changedCount = await super.removeOrphanAiFiles(existingFiles, generatedFiles);
|
|
13168
|
+
changedCount += await this.retractGooseSlashCommands(generatedFiles);
|
|
12901
13169
|
if (!shouldDisableHermesCommandsPlugin) return changedCount;
|
|
12902
13170
|
const configPath = (0, node_path.join)(this.outputRoot, getHermesagentRelativeFilePath({
|
|
12903
13171
|
global: this.global,
|
|
@@ -12916,6 +13184,27 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
12916
13184
|
return changedCount;
|
|
12917
13185
|
}
|
|
12918
13186
|
/**
|
|
13187
|
+
* Drop the `slash_commands` registrations when no Goose recipe is generated
|
|
13188
|
+
* any more. `GooseCommand.getAuxiliaryFiles` handles every other case, but it
|
|
13189
|
+
* is not reached when the whole feature has no source files left (`--delete`
|
|
13190
|
+
* removes the recipes there), which would strand `/name` on a deleted recipe.
|
|
13191
|
+
*/
|
|
13192
|
+
async retractGooseSlashCommands(generatedFiles) {
|
|
13193
|
+
if (this.toolTarget !== "goose" || !this.global) return 0;
|
|
13194
|
+
if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
|
|
13195
|
+
const configPath = (0, node_path.join)(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
|
|
13196
|
+
const currentContent = await readFileContentOrNull(configPath);
|
|
13197
|
+
if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
|
|
13198
|
+
const nextContent = getGooseSlashCommandsConfigContent({
|
|
13199
|
+
currentContent,
|
|
13200
|
+
entries: []
|
|
13201
|
+
});
|
|
13202
|
+
if (nextContent === currentContent) return 0;
|
|
13203
|
+
if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
|
|
13204
|
+
else await writeFileContent(configPath, nextContent);
|
|
13205
|
+
return 1;
|
|
13206
|
+
}
|
|
13207
|
+
/**
|
|
12919
13208
|
* Implementation of abstract method from FeatureProcessor
|
|
12920
13209
|
* Return the tool targets that this processor supports
|
|
12921
13210
|
*/
|
|
@@ -13084,6 +13373,14 @@ var ToolHooks = class extends ToolFile {
|
|
|
13084
13373
|
static async getAuxiliaryFiles(_params) {
|
|
13085
13374
|
return [];
|
|
13086
13375
|
}
|
|
13376
|
+
/**
|
|
13377
|
+
* Extra files the deletion sweep may remove, for adapters that write more
|
|
13378
|
+
* than their settable path. Kept separate from {@link getAuxiliaryFiles},
|
|
13379
|
+
* which may legitimately return a shared user-owned config file.
|
|
13380
|
+
*/
|
|
13381
|
+
static async getDeletableAuxiliaryFiles(_params) {
|
|
13382
|
+
return [];
|
|
13383
|
+
}
|
|
13087
13384
|
};
|
|
13088
13385
|
//#endregion
|
|
13089
13386
|
//#region src/features/hooks/amp-hooks.ts
|
|
@@ -14366,8 +14663,7 @@ const CLAUDE_CONVERTER_CONFIG = {
|
|
|
14366
14663
|
"teammateIdle",
|
|
14367
14664
|
"cwdChanged",
|
|
14368
14665
|
"beforeSubmitPrompt",
|
|
14369
|
-
"stop"
|
|
14370
|
-
"directoryAdded"
|
|
14666
|
+
"stop"
|
|
14371
14667
|
]),
|
|
14372
14668
|
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
14373
14669
|
"command",
|
|
@@ -14541,6 +14837,281 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
|
14541
14837
|
}
|
|
14542
14838
|
};
|
|
14543
14839
|
//#endregion
|
|
14840
|
+
//#region src/features/hooks/cline-hooks-generator.ts
|
|
14841
|
+
/**
|
|
14842
|
+
* Marker line every generated hook script carries. Cline resolves hooks by
|
|
14843
|
+
* exact event name from a directory users also hand-author scripts in, so the
|
|
14844
|
+
* marker is what tells a rulesync-owned script apart from a user's own: only
|
|
14845
|
+
* files carrying it are rewritten or cleaned up.
|
|
14846
|
+
*/
|
|
14847
|
+
const CLINE_HOOK_SCRIPT_MARKER = "rulesync-owned: cline-hooks";
|
|
14848
|
+
/** Exit code a hook command uses to cancel the task (Claude Code convention). */
|
|
14849
|
+
const CANCEL_EXIT_CODE = 2;
|
|
14850
|
+
function sanitizeCommand(command) {
|
|
14851
|
+
let sanitized = command;
|
|
14852
|
+
for (const char of CONTROL_CHARS) sanitized = sanitized.replaceAll(char, "");
|
|
14853
|
+
return sanitized;
|
|
14854
|
+
}
|
|
14855
|
+
/** Single-quote a string for POSIX shells. */
|
|
14856
|
+
function shellQuote(value) {
|
|
14857
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
14858
|
+
}
|
|
14859
|
+
/** Single-quote a string for PowerShell. */
|
|
14860
|
+
function powerShellQuote(value) {
|
|
14861
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
14862
|
+
}
|
|
14863
|
+
function collectClineHookCommands({ effectiveHooks, eventMap }) {
|
|
14864
|
+
const commandsByEvent = {};
|
|
14865
|
+
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
14866
|
+
const clineEvent = eventMap[canonicalEvent];
|
|
14867
|
+
if (!clineEvent) continue;
|
|
14868
|
+
const commands = definitions.filter((definition) => (definition.type ?? "command") === "command" && definition.command).map((definition) => sanitizeCommand(definition.command)).filter((command) => command.trim() !== "");
|
|
14869
|
+
if (commands.length === 0) continue;
|
|
14870
|
+
const existing = commandsByEvent[clineEvent];
|
|
14871
|
+
if (existing) existing.push(...commands);
|
|
14872
|
+
else commandsByEvent[clineEvent] = commands;
|
|
14873
|
+
}
|
|
14874
|
+
return commandsByEvent;
|
|
14875
|
+
}
|
|
14876
|
+
/**
|
|
14877
|
+
* A POSIX wrapper script for one Cline hook event.
|
|
14878
|
+
*
|
|
14879
|
+
* Cline spawns the file itself (`spawn(scriptPath, [], { shell: true })`), feeds
|
|
14880
|
+
* the event payload on stdin and reads a JSON result from stdout. The wrapper
|
|
14881
|
+
* therefore forwards the payload to each configured command in order and
|
|
14882
|
+
* translates the exit codes: `2` cancels the task, any other failure is
|
|
14883
|
+
* reported through `errorMessage` without cancelling, mirroring how the
|
|
14884
|
+
* canonical `command` hook type behaves for the tools that support blocking.
|
|
14885
|
+
*/
|
|
14886
|
+
function generateClineHookScript({ event, commands }) {
|
|
14887
|
+
const lines = [
|
|
14888
|
+
"#!/bin/bash",
|
|
14889
|
+
`# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
|
|
14890
|
+
`# ${CLINE_HOOK_SCRIPT_MARKER}`,
|
|
14891
|
+
"",
|
|
14892
|
+
"payload=$(cat)",
|
|
14893
|
+
"cancel=false",
|
|
14894
|
+
"error_message=''",
|
|
14895
|
+
""
|
|
14896
|
+
];
|
|
14897
|
+
for (const command of commands) {
|
|
14898
|
+
const quoted = shellQuote(command);
|
|
14899
|
+
lines.push("if [ \"$cancel\" = false ]; then", ` if bash -n -c ${quoted} 2>/dev/null; then`, ` hook_stderr=$(printf '%s' "$payload" | bash -c ${quoted} 2>&1 >/dev/null)`, " hook_status=$?", ` if [ "$hook_status" -eq ${CANCEL_EXIT_CODE} ]; then`, " cancel=true", " error_message=\"$hook_stderr\"", " elif [ \"$hook_status\" -ne 0 ]; then", ` printf '%s\\n' "rulesync ${event} hook failed (exit $hook_status): $hook_stderr" >&2`, " error_message=\"$hook_stderr\"", " fi", " else", ` error_message="rulesync ${event} hook command is not valid shell syntax"`, ` printf '%s\\n' "$error_message" >&2`, " fi", "fi", "");
|
|
14900
|
+
}
|
|
14901
|
+
lines.push("escape_json() {", ` printf '%s' "$1" | tr '\\n\\r\\t' ' ' | tr -d '\\000-\\037' | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'`, "}", "", `printf '{"cancel": %s, "contextModification": "", "errorMessage": "%s"}\\n' "$cancel" "$(escape_json "$error_message")"`, "");
|
|
14902
|
+
return lines.join("\n");
|
|
14903
|
+
}
|
|
14904
|
+
/**
|
|
14905
|
+
* The PowerShell twin of {@link generateClineHookScript}. On Windows Cline
|
|
14906
|
+
* resolves only `<Event>.ps1` and runs it through `powershell -File`, so both
|
|
14907
|
+
* spellings are written and the platform picks one.
|
|
14908
|
+
*/
|
|
14909
|
+
function generateClineHookPowerShellScript({ event, commands }) {
|
|
14910
|
+
const lines = [
|
|
14911
|
+
`# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
|
|
14912
|
+
`# ${CLINE_HOOK_SCRIPT_MARKER}`,
|
|
14913
|
+
"",
|
|
14914
|
+
"$payload = [Console]::In.ReadToEnd()",
|
|
14915
|
+
"$cancel = $false",
|
|
14916
|
+
"$errorMessage = ''",
|
|
14917
|
+
""
|
|
14918
|
+
];
|
|
14919
|
+
for (const command of commands) lines.push("if (-not $cancel) {", ` $hookStderr = ($payload | & cmd /c ${powerShellQuote(command)} 2>&1 | Out-String)`, " $hookStatus = $LASTEXITCODE", ` if ($hookStatus -eq ${CANCEL_EXIT_CODE}) {`, " $cancel = $true", " $errorMessage = $hookStderr", " } elseif ($hookStatus -ne 0) {", ` Write-Error ${powerShellQuote(`rulesync ${event} hook failed`)}`, " $errorMessage = $hookStderr", " }", "}", "");
|
|
14920
|
+
lines.push("@{", " cancel = $cancel", " contextModification = \"\"", " errorMessage = $errorMessage", "} | ConvertTo-Json -Compress", "");
|
|
14921
|
+
return lines.join("\n");
|
|
14922
|
+
}
|
|
14923
|
+
//#endregion
|
|
14924
|
+
//#region src/features/hooks/cline-hooks.ts
|
|
14925
|
+
/** Mode Cline's hook scripts need: it spawns the file itself on Unix. */
|
|
14926
|
+
const HOOK_SCRIPT_MODE = 493;
|
|
14927
|
+
/** The only file names this adapter ever writes into the hooks directory. */
|
|
14928
|
+
const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES));
|
|
14929
|
+
/**
|
|
14930
|
+
* Read the manifest of a previous run. Event names are filtered against the
|
|
14931
|
+
* names this adapter emits: the manifest is a file in the repository, and
|
|
14932
|
+
* anything else there would otherwise be turned into a path — an executable one
|
|
14933
|
+
* — that rulesync writes.
|
|
14934
|
+
*/
|
|
14935
|
+
function parseManifest(fileContent) {
|
|
14936
|
+
try {
|
|
14937
|
+
const parsed = JSON.parse(fileContent);
|
|
14938
|
+
if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
|
|
14939
|
+
return {
|
|
14940
|
+
generatedBy: "rulesync",
|
|
14941
|
+
events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
|
|
14942
|
+
};
|
|
14943
|
+
} catch {
|
|
14944
|
+
return null;
|
|
14945
|
+
}
|
|
14946
|
+
}
|
|
14947
|
+
/** One generated hook script. Written executable so Cline can spawn it. */
|
|
14948
|
+
var ClineHookScript = class extends ToolFile {
|
|
14949
|
+
getFileMode() {
|
|
14950
|
+
return this.getRelativeFilePath().endsWith(".ps1") ? void 0 : HOOK_SCRIPT_MODE;
|
|
14951
|
+
}
|
|
14952
|
+
validate() {
|
|
14953
|
+
return {
|
|
14954
|
+
success: true,
|
|
14955
|
+
error: null
|
|
14956
|
+
};
|
|
14957
|
+
}
|
|
14958
|
+
};
|
|
14959
|
+
/**
|
|
14960
|
+
* Hooks adapter for Cline's file-based hooks.
|
|
14961
|
+
*
|
|
14962
|
+
* Cline (VS Code extension / Cline Desktop) resolves one executable per
|
|
14963
|
+
* lifecycle event from `<project>/.clinerules/hooks/` or the global
|
|
14964
|
+
* `~/Documents/Cline/Hooks/`, named exactly after the event: the extensionless
|
|
14965
|
+
* name on Unix, `<Event>.ps1` on Windows. The script receives the event payload
|
|
14966
|
+
* as JSON on stdin and answers with `{"cancel": …, "contextModification": …,
|
|
14967
|
+
* "errorMessage": …}` on stdout. rulesync emits a wrapper script per configured
|
|
14968
|
+
* event in both spellings, plus a `rulesync-hooks.json` manifest naming the
|
|
14969
|
+
* scripts it owns.
|
|
14970
|
+
*
|
|
14971
|
+
* The directory is shared with hand-authored hooks and the filenames are fixed
|
|
14972
|
+
* by the contract, so every generated script carries a marker line and a script
|
|
14973
|
+
* without it is never overwritten.
|
|
14974
|
+
*
|
|
14975
|
+
* Cline's CLI and SDK use a different, in-process hook surface (`AgentHooks`
|
|
14976
|
+
* from `@cline/core`), which this adapter does not target.
|
|
14977
|
+
*
|
|
14978
|
+
* @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
|
|
14979
|
+
*/
|
|
14980
|
+
var ClineHooks = class ClineHooks extends ToolHooks {
|
|
14981
|
+
scriptsByEvent;
|
|
14982
|
+
constructor(params) {
|
|
14983
|
+
super({
|
|
14984
|
+
...params,
|
|
14985
|
+
fileContent: params.fileContent ?? ""
|
|
14986
|
+
});
|
|
14987
|
+
this.scriptsByEvent = params.scriptsByEvent ?? {};
|
|
14988
|
+
}
|
|
14989
|
+
static getSettablePaths(options) {
|
|
14990
|
+
return {
|
|
14991
|
+
relativeDirPath: options?.global ? CLINE_HOOKS_GLOBAL_DIR_PATH : CLINE_HOOKS_DIR_PATH,
|
|
14992
|
+
relativeFilePath: CLINE_HOOKS_MANIFEST_FILE_NAME
|
|
14993
|
+
};
|
|
14994
|
+
}
|
|
14995
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14996
|
+
const paths = ClineHooks.getSettablePaths({ global });
|
|
14997
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
14998
|
+
return new ClineHooks({
|
|
14999
|
+
outputRoot,
|
|
15000
|
+
...paths,
|
|
15001
|
+
fileContent,
|
|
15002
|
+
validate
|
|
15003
|
+
});
|
|
15004
|
+
}
|
|
15005
|
+
static fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
15006
|
+
const config = rulesyncHooks.getJson();
|
|
15007
|
+
const overrideHooks = config.cline?.hooks ?? {};
|
|
15008
|
+
const scriptsByEvent = collectClineHookCommands({
|
|
15009
|
+
effectiveHooks: {
|
|
15010
|
+
...config.hooks,
|
|
15011
|
+
...overrideHooks
|
|
15012
|
+
},
|
|
15013
|
+
eventMap: CANONICAL_TO_CLINE_EVENT_NAMES
|
|
15014
|
+
});
|
|
15015
|
+
const manifest = {
|
|
15016
|
+
generatedBy: "rulesync",
|
|
15017
|
+
events: Object.keys(scriptsByEvent).toSorted()
|
|
15018
|
+
};
|
|
15019
|
+
return new ClineHooks({
|
|
15020
|
+
outputRoot,
|
|
15021
|
+
...ClineHooks.getSettablePaths({ global }),
|
|
15022
|
+
fileContent: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
15023
|
+
validate,
|
|
15024
|
+
scriptsByEvent
|
|
15025
|
+
});
|
|
15026
|
+
}
|
|
15027
|
+
/**
|
|
15028
|
+
* The per-event scripts, plus a neutralized script for every event a previous
|
|
15029
|
+
* run generated and this one no longer covers — those files stay on disk (the
|
|
15030
|
+
* hooks feature only reconciles its single settable path), so they are
|
|
15031
|
+
* rewritten as no-ops instead of being left running a removed hook.
|
|
15032
|
+
*/
|
|
15033
|
+
async getScriptFiles({ global = false, logger } = {}) {
|
|
15034
|
+
const paths = ClineHooks.getSettablePaths({ global });
|
|
15035
|
+
const previous = parseManifest(await readFileContentOrNull((0, node_path.join)(this.outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "")?.events ?? [];
|
|
15036
|
+
const events = [.../* @__PURE__ */ new Set([...Object.keys(this.scriptsByEvent), ...previous])].toSorted();
|
|
15037
|
+
const files = [];
|
|
15038
|
+
for (const event of events) {
|
|
15039
|
+
const commands = this.scriptsByEvent[event] ?? [];
|
|
15040
|
+
for (const [relativeFilePath, fileContent] of [[event, generateClineHookScript({
|
|
15041
|
+
event,
|
|
15042
|
+
commands
|
|
15043
|
+
})], [`${event}.ps1`, generateClineHookPowerShellScript({
|
|
15044
|
+
event,
|
|
15045
|
+
commands
|
|
15046
|
+
})]]) {
|
|
15047
|
+
const existing = await readFileContentOrNull((0, node_path.join)(this.outputRoot, paths.relativeDirPath, relativeFilePath));
|
|
15048
|
+
if (existing !== null && !existing.includes("rulesync-owned: cline-hooks")) {
|
|
15049
|
+
logger?.warn(`Kept the existing ${(0, node_path.join)(paths.relativeDirPath, relativeFilePath)}: it was not generated by rulesync, so the ${event} hook from .rulesync/hooks.jsonc is not written. Remove or rename that file to let rulesync manage the event.`);
|
|
15050
|
+
continue;
|
|
15051
|
+
}
|
|
15052
|
+
files.push(new ClineHookScript({
|
|
15053
|
+
outputRoot: this.outputRoot,
|
|
15054
|
+
relativeDirPath: paths.relativeDirPath,
|
|
15055
|
+
relativeFilePath,
|
|
15056
|
+
fileContent
|
|
15057
|
+
}));
|
|
15058
|
+
}
|
|
15059
|
+
}
|
|
15060
|
+
return files;
|
|
15061
|
+
}
|
|
15062
|
+
/**
|
|
15063
|
+
* The generated scripts ride alongside the manifest. Only a `ClineHooks`
|
|
15064
|
+
* instance can produce them, so the processor hands its freshly built one
|
|
15065
|
+
* back here.
|
|
15066
|
+
*/
|
|
15067
|
+
static async getAuxiliaryFiles({ global = false, toolHooks, logger } = {}) {
|
|
15068
|
+
if (!(toolHooks instanceof ClineHooks)) return [];
|
|
15069
|
+
return toolHooks.getScriptFiles({
|
|
15070
|
+
global,
|
|
15071
|
+
logger
|
|
15072
|
+
});
|
|
15073
|
+
}
|
|
15074
|
+
/**
|
|
15075
|
+
* Every rulesync-marked script currently on disk. Dropping the target must
|
|
15076
|
+
* take the scripts with it: they hold the actual commands, and leaving them
|
|
15077
|
+
* behind keeps a removed hook running.
|
|
15078
|
+
*/
|
|
15079
|
+
static async getDeletableAuxiliaryFiles({ outputRoot = process.cwd(), global = false } = {}) {
|
|
15080
|
+
const paths = ClineHooks.getSettablePaths({ global });
|
|
15081
|
+
const files = [];
|
|
15082
|
+
for (const event of [...MANAGED_EVENT_NAMES].toSorted()) for (const relativeFilePath of [event, `${event}.ps1`]) {
|
|
15083
|
+
const existing = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath));
|
|
15084
|
+
if (existing === null || !existing.includes("rulesync-owned: cline-hooks")) continue;
|
|
15085
|
+
files.push(new ClineHookScript({
|
|
15086
|
+
outputRoot,
|
|
15087
|
+
relativeDirPath: paths.relativeDirPath,
|
|
15088
|
+
relativeFilePath,
|
|
15089
|
+
fileContent: "",
|
|
15090
|
+
validate: false
|
|
15091
|
+
}));
|
|
15092
|
+
}
|
|
15093
|
+
return files;
|
|
15094
|
+
}
|
|
15095
|
+
toRulesyncHooks() {
|
|
15096
|
+
throw new Error("Not implemented because generated Cline hook scripts cannot be imported back into canonical hooks.");
|
|
15097
|
+
}
|
|
15098
|
+
validate() {
|
|
15099
|
+
return {
|
|
15100
|
+
success: true,
|
|
15101
|
+
error: null
|
|
15102
|
+
};
|
|
15103
|
+
}
|
|
15104
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
15105
|
+
return new ClineHooks({
|
|
15106
|
+
outputRoot,
|
|
15107
|
+
relativeDirPath,
|
|
15108
|
+
relativeFilePath,
|
|
15109
|
+
fileContent: "",
|
|
15110
|
+
validate: false
|
|
15111
|
+
});
|
|
15112
|
+
}
|
|
15113
|
+
};
|
|
15114
|
+
//#endregion
|
|
14544
15115
|
//#region src/features/hooks/codexcli-hooks.ts
|
|
14545
15116
|
const CODEXCLI_CONVERTER_CONFIG = {
|
|
14546
15117
|
supportedEvents: CODEXCLI_HOOK_EVENTS,
|
|
@@ -14707,12 +15278,19 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14707
15278
|
/**
|
|
14708
15279
|
* Copilot hook entry as stored in .github/hooks/copilot-hooks.json.
|
|
14709
15280
|
*
|
|
14710
|
-
*
|
|
15281
|
+
* The canonical `shell` selector chooses `bash` or `powershell`; without it the
|
|
15282
|
+
* portable `command` field is written, which upstream copies to both when
|
|
15283
|
+
* neither is present. Note the cloud agent runs hooks in a Linux sandbox and
|
|
15284
|
+
* honors only `bash` and `command` — a `powershell` entry is ignored there.
|
|
15285
|
+
*
|
|
15286
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
14711
15287
|
*/
|
|
14712
15288
|
const CopilotHookEntrySchema = zod_mini.z.looseObject({
|
|
14713
15289
|
type: zod_mini.z.string(),
|
|
14714
15290
|
bash: zod_mini.z.optional(zod_mini.z.string()),
|
|
14715
15291
|
powershell: zod_mini.z.optional(zod_mini.z.string()),
|
|
15292
|
+
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
15293
|
+
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
14716
15294
|
timeoutSec: zod_mini.z.optional(zod_mini.z.number())
|
|
14717
15295
|
});
|
|
14718
15296
|
/**
|
|
@@ -14720,12 +15298,16 @@ const CopilotHookEntrySchema = zod_mini.z.looseObject({
|
|
|
14720
15298
|
* Filters shared hooks to COPILOT_HOOK_EVENTS, merges config.copilot?.hooks,
|
|
14721
15299
|
* then converts to Copilot event names and field format.
|
|
14722
15300
|
*
|
|
14723
|
-
*
|
|
14724
|
-
*
|
|
15301
|
+
* The command field is chosen by the canonical `shell` selector, falling back to
|
|
15302
|
+
* the portable `command` field — never by the platform Rulesync happens to run
|
|
15303
|
+
* on. Keying it off `process.platform` meant a file generated on Windows carried
|
|
15304
|
+
* only `powershell`, which the Linux-sandboxed cloud agent ignores outright, so
|
|
15305
|
+
* the hook silently never ran; it also made the output differ per generating
|
|
15306
|
+
* machine, which shows up as churn for anyone who checks the file in (the cloud
|
|
15307
|
+
* agent reads it from the repository).
|
|
14725
15308
|
*/
|
|
14726
15309
|
function canonicalToCopilotHooks(config) {
|
|
14727
15310
|
const canonicalSchemaKeys = Object.keys(HookDefinitionSchema.shape);
|
|
14728
|
-
const commandField = process.platform === "win32" ? "powershell" : "bash";
|
|
14729
15311
|
const supported = new Set(COPILOT_HOOK_EVENTS);
|
|
14730
15312
|
const sharedConfigHooks = {};
|
|
14731
15313
|
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedConfigHooks[event] = defs;
|
|
@@ -14743,10 +15325,12 @@ function canonicalToCopilotHooks(config) {
|
|
|
14743
15325
|
if (hookType !== "command") continue;
|
|
14744
15326
|
const command = def.command;
|
|
14745
15327
|
const timeout = def.timeout;
|
|
15328
|
+
const commandField = def.shell ?? "command";
|
|
14746
15329
|
const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
|
|
14747
15330
|
entries.push({
|
|
14748
15331
|
type: hookType,
|
|
14749
15332
|
...command !== void 0 && command !== null && { [commandField]: command },
|
|
15333
|
+
...def.env !== void 0 && { env: def.env },
|
|
14750
15334
|
...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
|
|
14751
15335
|
...rest
|
|
14752
15336
|
});
|
|
@@ -14756,24 +15340,35 @@ function canonicalToCopilotHooks(config) {
|
|
|
14756
15340
|
return copilot;
|
|
14757
15341
|
}
|
|
14758
15342
|
/**
|
|
14759
|
-
* Resolve the command
|
|
15343
|
+
* Resolve the command and its shell selector from a Copilot hook entry.
|
|
14760
15344
|
*
|
|
14761
|
-
* - If only
|
|
14762
|
-
* -
|
|
14763
|
-
* - If both are present,
|
|
14764
|
-
*
|
|
15345
|
+
* - If only one shell-specific field is present, use it and record the shell,
|
|
15346
|
+
* so a re-export writes the same field back.
|
|
15347
|
+
* - If both are present, take `bash` and warn. The choice is deliberately not
|
|
15348
|
+
* platform-dependent: the cloud agent runs hooks in a Linux sandbox and
|
|
15349
|
+
* ignores `powershell` entirely, and importing on Windows must not produce a
|
|
15350
|
+
* different canonical config than importing the same file on Linux.
|
|
15351
|
+
* - Otherwise fall back to the portable `command` field, leaving `shell` unset
|
|
15352
|
+
* so a re-export renders the portable field again.
|
|
14765
15353
|
*/
|
|
14766
15354
|
function resolveImportCommand$1(entry, logger) {
|
|
14767
15355
|
const hasBash = typeof entry.bash === "string";
|
|
14768
15356
|
const hasPowershell = typeof entry.powershell === "string";
|
|
14769
15357
|
if (hasBash && hasPowershell) {
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
} else if (hasBash) return
|
|
14776
|
-
|
|
15358
|
+
logger?.warn("Copilot hook has both bash and powershell commands; using bash and ignoring powershell, which the Linux-sandboxed cloud agent does not run.");
|
|
15359
|
+
return {
|
|
15360
|
+
command: entry.bash,
|
|
15361
|
+
shell: "bash"
|
|
15362
|
+
};
|
|
15363
|
+
} else if (hasBash) return {
|
|
15364
|
+
command: entry.bash,
|
|
15365
|
+
shell: "bash"
|
|
15366
|
+
};
|
|
15367
|
+
else if (hasPowershell) return {
|
|
15368
|
+
command: entry.powershell,
|
|
15369
|
+
shell: "powershell"
|
|
15370
|
+
};
|
|
15371
|
+
return typeof entry.command === "string" ? { command: entry.command } : {};
|
|
14777
15372
|
}
|
|
14778
15373
|
/**
|
|
14779
15374
|
* Extract hooks from Copilot hooks JSON into canonical format.
|
|
@@ -14790,11 +15385,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
14790
15385
|
const parseResult = CopilotHookEntrySchema.safeParse(rawEntry);
|
|
14791
15386
|
if (!parseResult.success) continue;
|
|
14792
15387
|
const entry = parseResult.data;
|
|
14793
|
-
const command = resolveImportCommand$1(entry, logger);
|
|
15388
|
+
const { command, shell } = resolveImportCommand$1(entry, logger);
|
|
14794
15389
|
const timeout = entry.timeoutSec;
|
|
14795
15390
|
defs.push({
|
|
14796
15391
|
type: "command",
|
|
14797
15392
|
...command !== void 0 && { command },
|
|
15393
|
+
...shell !== void 0 && { shell },
|
|
15394
|
+
...entry.env !== void 0 && { env: entry.env },
|
|
14798
15395
|
...timeout !== void 0 && { timeout }
|
|
14799
15396
|
});
|
|
14800
15397
|
}
|
|
@@ -15953,6 +16550,27 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
|
|
|
15953
16550
|
const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
|
|
15954
16551
|
const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
|
|
15955
16552
|
/**
|
|
16553
|
+
* Whether an entry of the `hooks:` mapping is a hook-event list rather than one
|
|
16554
|
+
* of its non-event siblings.
|
|
16555
|
+
*
|
|
16556
|
+
* The mapping is not all events: Hermes v0.20.0 nests the outbound webhook
|
|
16557
|
+
* registry there as `hooks.outbound`, a list of targets (`name`, `url`,
|
|
16558
|
+
* `events`, `secret_env`, `matcher`, `timeout`) that rulesync neither authors
|
|
16559
|
+
* nor imports. A documented native event is an event whatever its value; for
|
|
16560
|
+
* anything else the value decides, because rulesync also emits *undocumented*
|
|
16561
|
+
* event names supplied through the `hermesagent.hooks` override (forward
|
|
16562
|
+
* compatibility), and those must stay retractable. Everything rulesync writes
|
|
16563
|
+
* is a non-empty list of entries carrying a string `command`, which no registry
|
|
16564
|
+
* entry has — `outbound` entries carry `url`/`events` instead.
|
|
16565
|
+
*
|
|
16566
|
+
* Both directions ask this one question, so import and generate cannot drift.
|
|
16567
|
+
* @see https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks
|
|
16568
|
+
*/
|
|
16569
|
+
function isHermesHookEventEntry(key, value) {
|
|
16570
|
+
if (HERMESAGENT_NATIVE_EVENTS.has(key)) return true;
|
|
16571
|
+
return Array.isArray(value) && value.length > 0 && value.every((entry) => isPlainObject$1(entry) && typeof entry.command === "string");
|
|
16572
|
+
}
|
|
16573
|
+
/**
|
|
15956
16574
|
* Convert the canonical hooks config into Hermes's native
|
|
15957
16575
|
* `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
|
|
15958
16576
|
*
|
|
@@ -16013,7 +16631,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
16013
16631
|
}
|
|
16014
16632
|
for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
16015
16633
|
if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
|
|
16016
|
-
if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.
|
|
16634
|
+
if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.20.0; preserving it for forward compatibility.`);
|
|
16017
16635
|
setHermesHookEntries({
|
|
16018
16636
|
result,
|
|
16019
16637
|
event: nativeEvent,
|
|
@@ -16035,6 +16653,7 @@ function hermesHooksToCanonical(hooks) {
|
|
|
16035
16653
|
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
16036
16654
|
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
16037
16655
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
16656
|
+
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
16038
16657
|
const rulesyncEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent] ?? nativeEvent;
|
|
16039
16658
|
const defs = [];
|
|
16040
16659
|
for (const raw of entries) {
|
|
@@ -16054,6 +16673,32 @@ function hermesHooksToCanonical(hooks) {
|
|
|
16054
16673
|
return canonical;
|
|
16055
16674
|
}
|
|
16056
16675
|
/**
|
|
16676
|
+
* Recompute the `hooks:` mapping that is written back to `config.yaml`.
|
|
16677
|
+
*
|
|
16678
|
+
* rulesync owns the hook events inside that mapping, but not the mapping
|
|
16679
|
+
* itself: Hermes v0.20.0 nests the outbound webhook registry under the same key
|
|
16680
|
+
* as `hooks.outbound`, and it is a list of webhook targets rather than a hook
|
|
16681
|
+
* event, so it has no rulesync spelling and no migration path. Replacing the
|
|
16682
|
+
* whole mapping destroyed it on every generate. Every key that is not an event
|
|
16683
|
+
* ({@link isHermesHookEventEntry}) is therefore carried over from the existing
|
|
16684
|
+
* file, while event keys are replaced wholesale so a hook deleted from the
|
|
16685
|
+
* rulesync source is retracted — including one written under an undocumented
|
|
16686
|
+
* event name through the `hermesagent.hooks` override.
|
|
16687
|
+
* @see https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks
|
|
16688
|
+
*/
|
|
16689
|
+
function mergeHermesHooksBlock({ existingHooks, generatedHooks }) {
|
|
16690
|
+
const preserved = {};
|
|
16691
|
+
if (isPlainObject$1(existingHooks)) for (const [key, value] of Object.entries(existingHooks)) {
|
|
16692
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
16693
|
+
if (isHermesHookEventEntry(key, value)) continue;
|
|
16694
|
+
preserved[key] = value;
|
|
16695
|
+
}
|
|
16696
|
+
return {
|
|
16697
|
+
...preserved,
|
|
16698
|
+
...isPlainObject$1(generatedHooks) ? generatedHooks : {}
|
|
16699
|
+
};
|
|
16700
|
+
}
|
|
16701
|
+
/**
|
|
16057
16702
|
* Hermes Agent shell hooks.
|
|
16058
16703
|
*
|
|
16059
16704
|
* Hermes Agent registers shell-command hooks under the `hooks:` key of the
|
|
@@ -16121,14 +16766,22 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
16121
16766
|
return true;
|
|
16122
16767
|
}
|
|
16123
16768
|
setFileContent(fileContent) {
|
|
16769
|
+
const existing = parseSharedConfig({
|
|
16770
|
+
format: "yaml",
|
|
16771
|
+
fileContent
|
|
16772
|
+
});
|
|
16773
|
+
const generated = parseSharedConfig({
|
|
16774
|
+
format: "yaml",
|
|
16775
|
+
fileContent: this.fileContent
|
|
16776
|
+
});
|
|
16124
16777
|
this.fileContent = applySharedConfigPatch({
|
|
16125
16778
|
fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
|
|
16126
16779
|
feature: "hooks",
|
|
16127
16780
|
existingContent: fileContent,
|
|
16128
|
-
patch:
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
})
|
|
16781
|
+
patch: { hooks: mergeHermesHooksBlock({
|
|
16782
|
+
existingHooks: existing.hooks,
|
|
16783
|
+
generatedHooks: generated.hooks
|
|
16784
|
+
}) }
|
|
16132
16785
|
});
|
|
16133
16786
|
}
|
|
16134
16787
|
toRulesyncHooks() {
|
|
@@ -16282,15 +16935,36 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
16282
16935
|
*
|
|
16283
16936
|
* `experimental.session.compacting` receives `(input, output)` and exposes no
|
|
16284
16937
|
* per-invocation identifier worth matching on, so it takes `null`.
|
|
16938
|
+
* `chat.message` receives `(input, output)` with the prompt text living in
|
|
16939
|
+
* `output.parts` rather than a single matchable field, so it takes `null` too.
|
|
16285
16940
|
*
|
|
16286
16941
|
* @see https://opencode.ai/docs/plugins/
|
|
16287
16942
|
*/
|
|
16288
16943
|
const NAMED_HOOK_MATCHER_SUBJECTS = {
|
|
16289
16944
|
"tool.execute.before": "input.tool",
|
|
16290
16945
|
"tool.execute.after": "input.tool",
|
|
16291
|
-
"experimental.session.compacting": null
|
|
16946
|
+
"experimental.session.compacting": null,
|
|
16947
|
+
"chat.message": null
|
|
16292
16948
|
};
|
|
16293
16949
|
/**
|
|
16950
|
+
* Canonical events whose generic (`event.type`) dispatch fires more broadly
|
|
16951
|
+
* than the canonical event means, mapped to the extra condition the generated
|
|
16952
|
+
* handler gates on. Keyed by canonical event like `SHELL_EVENT_TOOL_GATES`, so
|
|
16953
|
+
* a second canonical event mapped onto the same dispatch does not inherit a
|
|
16954
|
+
* gate meant for its sibling.
|
|
16955
|
+
*
|
|
16956
|
+
* `permission.replied` fires for every reply — `once`, `always` and `reject` —
|
|
16957
|
+
* so the canonical `permissionDenied` handler runs only for a rejecting reply.
|
|
16958
|
+
*
|
|
16959
|
+
* Note the v1 SDK's generated `Event` typing still describes this payload as
|
|
16960
|
+
* `{ permissionID, response }`; the schema source, the v2 typings and the TUI's
|
|
16961
|
+
* live consumer all agree on `{ requestID, reply }`, so the stale codegen is
|
|
16962
|
+
* not followed here.
|
|
16963
|
+
*
|
|
16964
|
+
* @see https://opencode.ai/docs/plugins/
|
|
16965
|
+
*/
|
|
16966
|
+
const GENERIC_EVENT_PROPERTY_GATES = { permissionDenied: "event.properties.reply === \"reject\"" };
|
|
16967
|
+
/**
|
|
16294
16968
|
* OpenCode (and Kilo) have no shell-execution lifecycle event — the
|
|
16295
16969
|
* `command.executed` event these canonical events were once mapped to is a
|
|
16296
16970
|
* *slash-command* event, so a hook wired there never fired on bash commands
|
|
@@ -16335,6 +17009,7 @@ function validateAndSanitizeMatcher(matcher) {
|
|
|
16335
17009
|
function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
|
|
16336
17010
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
16337
17011
|
const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
|
|
17012
|
+
const propertyGate = GENERIC_EVENT_PROPERTY_GATES[canonicalEvent];
|
|
16338
17013
|
const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
|
|
16339
17014
|
if (!toolEvent) continue;
|
|
16340
17015
|
const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
|
|
@@ -16346,7 +17021,8 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
|
|
|
16346
17021
|
handlers.push({
|
|
16347
17022
|
command: def.command,
|
|
16348
17023
|
matcher: def.matcher ? def.matcher : void 0,
|
|
16349
|
-
...shellGate ? { toolGate: shellGate.tool } : {}
|
|
17024
|
+
...shellGate ? { toolGate: shellGate.tool } : {},
|
|
17025
|
+
...propertyGate ? { propertyGate } : {}
|
|
16350
17026
|
});
|
|
16351
17027
|
}
|
|
16352
17028
|
if (handlers.length > 0) {
|
|
@@ -16368,7 +17044,11 @@ function buildGenericEventBodyLines(genericEventHandlers) {
|
|
|
16368
17044
|
isFirst = false;
|
|
16369
17045
|
for (const handler of handlers) {
|
|
16370
17046
|
const escapedCommand = escapeForTemplateLiteral(handler.command);
|
|
16371
|
-
|
|
17047
|
+
if (handler.propertyGate) {
|
|
17048
|
+
bodyLines.push(` if (${handler.propertyGate}) {`);
|
|
17049
|
+
bodyLines.push(` await $\`${escapedCommand}\`;`);
|
|
17050
|
+
bodyLines.push(" }");
|
|
17051
|
+
} else bodyLines.push(` await $\`${escapedCommand}\`;`);
|
|
16372
17052
|
}
|
|
16373
17053
|
bodyLines.push(" }");
|
|
16374
17054
|
}
|
|
@@ -16736,13 +17416,246 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
|
|
|
16736
17416
|
}
|
|
16737
17417
|
};
|
|
16738
17418
|
//#endregion
|
|
17419
|
+
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
17420
|
+
/**
|
|
17421
|
+
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
17422
|
+
*
|
|
17423
|
+
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
17424
|
+
* imports do not drop data they do not yet understand.
|
|
17425
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
17426
|
+
*/
|
|
17427
|
+
const KiroIdeHookActionSchema = zod_mini.z.union([zod_mini.z.looseObject({
|
|
17428
|
+
type: zod_mini.z.literal("command"),
|
|
17429
|
+
command: zod_mini.z.optional(safeString)
|
|
17430
|
+
}), zod_mini.z.looseObject({
|
|
17431
|
+
type: zod_mini.z.literal("agent"),
|
|
17432
|
+
prompt: zod_mini.z.optional(safeString)
|
|
17433
|
+
})]);
|
|
17434
|
+
const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
|
|
17435
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
17436
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
17437
|
+
trigger: zod_mini.z.optional(zod_mini.z.string()),
|
|
17438
|
+
matcher: zod_mini.z.optional(zod_mini.z.string()),
|
|
17439
|
+
action: zod_mini.z.optional(KiroIdeHookActionSchema),
|
|
17440
|
+
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
17441
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
17442
|
+
});
|
|
17443
|
+
const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
|
|
17444
|
+
version: zod_mini.z.optional(zod_mini.z.string()),
|
|
17445
|
+
hooks: zod_mini.z.optional(zod_mini.z.array(KiroIdeHookEntrySchema))
|
|
17446
|
+
});
|
|
17447
|
+
/**
|
|
17448
|
+
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
17449
|
+
*
|
|
17450
|
+
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
17451
|
+
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
17452
|
+
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
17453
|
+
* already warns about unsupported types).
|
|
17454
|
+
*/
|
|
17455
|
+
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
17456
|
+
const entries = [];
|
|
17457
|
+
for (const def of definitions) {
|
|
17458
|
+
const type = def.type ?? "command";
|
|
17459
|
+
let action;
|
|
17460
|
+
if (type === "command") {
|
|
17461
|
+
if (def.command === void 0) continue;
|
|
17462
|
+
action = {
|
|
17463
|
+
type: "command",
|
|
17464
|
+
command: def.command
|
|
17465
|
+
};
|
|
17466
|
+
} else if (type === "prompt") {
|
|
17467
|
+
if (def.prompt === void 0) continue;
|
|
17468
|
+
action = {
|
|
17469
|
+
type: "agent",
|
|
17470
|
+
prompt: def.prompt
|
|
17471
|
+
};
|
|
17472
|
+
} else continue;
|
|
17473
|
+
entries.push({
|
|
17474
|
+
name: def.name ?? trigger,
|
|
17475
|
+
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
17476
|
+
trigger,
|
|
17477
|
+
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
17478
|
+
action,
|
|
17479
|
+
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
17480
|
+
enabled: def.enabled ?? true
|
|
17481
|
+
});
|
|
17482
|
+
}
|
|
17483
|
+
return entries;
|
|
17484
|
+
}
|
|
17485
|
+
function canonicalToKiroIdeHooks(config, overrideKey) {
|
|
17486
|
+
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
17487
|
+
const sharedHooks = {};
|
|
17488
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
17489
|
+
const effectiveHooks = {
|
|
17490
|
+
...sharedHooks,
|
|
17491
|
+
...config[overrideKey]?.hooks
|
|
17492
|
+
};
|
|
17493
|
+
const entries = [];
|
|
17494
|
+
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
17495
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
17496
|
+
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
17497
|
+
}
|
|
17498
|
+
return entries;
|
|
17499
|
+
}
|
|
17500
|
+
function kiroIdeHooksToCanonical(entries) {
|
|
17501
|
+
const canonical = {};
|
|
17502
|
+
for (const entry of entries) {
|
|
17503
|
+
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
17504
|
+
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
17505
|
+
if (isPrototypePollutionKey(eventName)) continue;
|
|
17506
|
+
const def = {};
|
|
17507
|
+
if (entry.action.type === "command") {
|
|
17508
|
+
if (!entry.action.command) continue;
|
|
17509
|
+
def.type = "command";
|
|
17510
|
+
def.command = entry.action.command;
|
|
17511
|
+
} else {
|
|
17512
|
+
if (!entry.action.prompt) continue;
|
|
17513
|
+
def.type = "prompt";
|
|
17514
|
+
def.prompt = entry.action.prompt;
|
|
17515
|
+
}
|
|
17516
|
+
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
17517
|
+
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
17518
|
+
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
17519
|
+
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
17520
|
+
if (entry.enabled === false) def.enabled = false;
|
|
17521
|
+
(canonical[eventName] ??= []).push(def);
|
|
17522
|
+
}
|
|
17523
|
+
return canonical;
|
|
17524
|
+
}
|
|
17525
|
+
/**
|
|
17526
|
+
* Hooks generator for the standalone Kiro hooks format (`.kiro/hooks/*.json`
|
|
17527
|
+
* v1), used by the **Kiro IDE** and, since Kiro CLI 3.0, by the CLI too.
|
|
17528
|
+
*
|
|
17529
|
+
* Kiro reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
17530
|
+
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
17531
|
+
* `hooks` array, so rulesync emits every generated hook into one
|
|
17532
|
+
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
17533
|
+
* which keeps it within the single-file hooks architecture.
|
|
17534
|
+
*
|
|
17535
|
+
* {@link import("./kiro-cli-hooks.js").KiroCliHooks} subclasses this to write
|
|
17536
|
+
* the same format for the `kiro-cli` target; only the deprecated `kiro` alias
|
|
17537
|
+
* still writes the embedded `.kiro/agents/default.json` agent-config shape,
|
|
17538
|
+
* which Kiro CLI 3.0 no longer reads.
|
|
17539
|
+
*
|
|
17540
|
+
* @see https://kiro.dev/docs/hooks/
|
|
17541
|
+
*/
|
|
17542
|
+
var KiroIdeHooks = class extends ToolHooks {
|
|
17543
|
+
constructor(params) {
|
|
17544
|
+
super({
|
|
17545
|
+
...params,
|
|
17546
|
+
fileContent: params.fileContent ?? JSON.stringify({
|
|
17547
|
+
version: "v1",
|
|
17548
|
+
hooks: []
|
|
17549
|
+
}, null, 2)
|
|
17550
|
+
});
|
|
17551
|
+
}
|
|
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
|
+
static getSettablePaths(_options = {}) {
|
|
17561
|
+
return {
|
|
17562
|
+
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
17563
|
+
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
17564
|
+
};
|
|
17565
|
+
}
|
|
17566
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
17567
|
+
const paths = this.getSettablePaths({ global });
|
|
17568
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
17569
|
+
version: "v1",
|
|
17570
|
+
hooks: []
|
|
17571
|
+
}, null, 2);
|
|
17572
|
+
return new this({
|
|
17573
|
+
outputRoot,
|
|
17574
|
+
relativeDirPath: paths.relativeDirPath,
|
|
17575
|
+
relativeFilePath: paths.relativeFilePath,
|
|
17576
|
+
fileContent,
|
|
17577
|
+
validate
|
|
17578
|
+
});
|
|
17579
|
+
}
|
|
17580
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
17581
|
+
const paths = this.getSettablePaths({ global });
|
|
17582
|
+
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson(), this.getOverrideKey());
|
|
17583
|
+
const fileContent = JSON.stringify({
|
|
17584
|
+
version: "v1",
|
|
17585
|
+
hooks
|
|
17586
|
+
}, null, 2);
|
|
17587
|
+
return new this({
|
|
17588
|
+
outputRoot,
|
|
17589
|
+
relativeDirPath: paths.relativeDirPath,
|
|
17590
|
+
relativeFilePath: paths.relativeFilePath,
|
|
17591
|
+
fileContent,
|
|
17592
|
+
validate
|
|
17593
|
+
});
|
|
17594
|
+
}
|
|
17595
|
+
toRulesyncHooks() {
|
|
17596
|
+
let parsed;
|
|
17597
|
+
try {
|
|
17598
|
+
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
17599
|
+
} catch (error) {
|
|
17600
|
+
throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17601
|
+
}
|
|
17602
|
+
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
17603
|
+
const overrideKey = this.constructor.getOverrideKey();
|
|
17604
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
17605
|
+
hooks,
|
|
17606
|
+
overrideKey
|
|
17607
|
+
}), null, 2) });
|
|
17608
|
+
}
|
|
17609
|
+
validate() {
|
|
17610
|
+
return {
|
|
17611
|
+
success: true,
|
|
17612
|
+
error: null
|
|
17613
|
+
};
|
|
17614
|
+
}
|
|
17615
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
17616
|
+
return new this({
|
|
17617
|
+
outputRoot,
|
|
17618
|
+
relativeDirPath,
|
|
17619
|
+
relativeFilePath,
|
|
17620
|
+
fileContent: JSON.stringify({
|
|
17621
|
+
version: "v1",
|
|
17622
|
+
hooks: []
|
|
17623
|
+
}, null, 2),
|
|
17624
|
+
validate: false
|
|
17625
|
+
});
|
|
17626
|
+
}
|
|
17627
|
+
};
|
|
17628
|
+
//#endregion
|
|
17629
|
+
//#region src/features/hooks/kiro-cli-hooks.ts
|
|
17630
|
+
/**
|
|
17631
|
+
* Hooks generator for the **Kiro CLI**.
|
|
17632
|
+
*
|
|
17633
|
+
* Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
|
|
17634
|
+
* Kiro IDE reads, so this reuses {@link KiroIdeHooks} and only redirects the
|
|
17635
|
+
* tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
17636
|
+
* the rulesync hooks config are honored, rather than `kiro-ide.hooks`).
|
|
17637
|
+
*
|
|
17638
|
+
* The embedded `.kiro/agents/default.json` agent-hook format this target used
|
|
17639
|
+
* to emit is documented as not working in 3.0, so it is left to the deprecated
|
|
17640
|
+
* `kiro` alias ({@link import("./kiro-hooks.js").KiroHooks}).
|
|
17641
|
+
*
|
|
17642
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
17643
|
+
* @see https://kiro.dev/docs/hooks/
|
|
17644
|
+
*/
|
|
17645
|
+
var KiroCliHooks = class extends KiroIdeHooks {
|
|
17646
|
+
static getOverrideKey() {
|
|
17647
|
+
return "kiro-cli";
|
|
17648
|
+
}
|
|
17649
|
+
};
|
|
17650
|
+
//#endregion
|
|
16739
17651
|
//#region src/features/hooks/kiro-hooks.ts
|
|
16740
17652
|
/**
|
|
16741
|
-
* Convert canonical hooks config to Kiro
|
|
17653
|
+
* Convert canonical hooks config to the legacy embedded Kiro agent-config
|
|
17654
|
+
* format.
|
|
16742
17655
|
* Filters shared hooks to KIRO_HOOK_EVENTS, merges config.kiro?.hooks,
|
|
16743
|
-
* then maps event names and emits
|
|
17656
|
+
* then maps event names and emits the agent config's hook arrays.
|
|
16744
17657
|
*/
|
|
16745
|
-
/** Build the
|
|
17658
|
+
/** Build the agent-config hook entries for a single canonical event's definitions. */
|
|
16746
17659
|
function buildKiroEntriesForEvent(definitions) {
|
|
16747
17660
|
const entries = [];
|
|
16748
17661
|
for (const def of definitions) {
|
|
@@ -16758,7 +17671,8 @@ function buildKiroEntriesForEvent(definitions) {
|
|
|
16758
17671
|
}
|
|
16759
17672
|
return entries;
|
|
16760
17673
|
}
|
|
16761
|
-
function canonicalToKiroHooks(config
|
|
17674
|
+
function canonicalToKiroHooks(config) {
|
|
17675
|
+
const overrideKey = "kiro";
|
|
16762
17676
|
const kiroSupported = new Set(KIRO_HOOK_EVENTS);
|
|
16763
17677
|
const sharedHooks = {};
|
|
16764
17678
|
for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
|
|
@@ -16776,8 +17690,8 @@ function canonicalToKiroHooks(config, overrideKey = "kiro") {
|
|
|
16776
17690
|
return kiro;
|
|
16777
17691
|
}
|
|
16778
17692
|
/**
|
|
16779
|
-
*
|
|
16780
|
-
* Uses `z.looseObject` so that unknown fields added by future Kiro
|
|
17693
|
+
* Hook entry as stored in each event's array of the agent config.
|
|
17694
|
+
* Uses `z.looseObject` so that unknown fields added by future Kiro
|
|
16781
17695
|
* versions are accepted and silently ignored during import.
|
|
16782
17696
|
*/
|
|
16783
17697
|
const KiroHookEntrySchema = zod_mini.z.looseObject({
|
|
@@ -16793,7 +17707,7 @@ function importCacheTtl(entry) {
|
|
|
16793
17707
|
return { cacheTtl: entry.cache_ttl_seconds };
|
|
16794
17708
|
}
|
|
16795
17709
|
/**
|
|
16796
|
-
* Extract hooks from Kiro
|
|
17710
|
+
* Extract hooks from the Kiro agent config into canonical format.
|
|
16797
17711
|
*/
|
|
16798
17712
|
function kiroHooksToCanonical(kiroHooks) {
|
|
16799
17713
|
if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
|
|
@@ -16821,6 +17735,17 @@ function kiroHooksToCanonical(kiroHooks) {
|
|
|
16821
17735
|
}
|
|
16822
17736
|
return canonical;
|
|
16823
17737
|
}
|
|
17738
|
+
/**
|
|
17739
|
+
* Hooks generator for the deprecated `kiro` alias: the embedded hook block of
|
|
17740
|
+
* `.kiro/agents/default.json`.
|
|
17741
|
+
*
|
|
17742
|
+
* Kiro's hooks migration guide states this format "does not work in 3.0", so
|
|
17743
|
+
* the `kiro-cli` target writes the standalone `.kiro/hooks/*.json` v1 format
|
|
17744
|
+
* instead ({@link import("./kiro-cli-hooks.js").KiroCliHooks}). It is kept here
|
|
17745
|
+
* so an existing agent config still round-trips.
|
|
17746
|
+
*
|
|
17747
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
17748
|
+
*/
|
|
16824
17749
|
var KiroHooks = class KiroHooks extends ToolHooks {
|
|
16825
17750
|
constructor(params) {
|
|
16826
17751
|
super({
|
|
@@ -16828,14 +17753,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16828
17753
|
fileContent: params.fileContent ?? "{}"
|
|
16829
17754
|
});
|
|
16830
17755
|
}
|
|
16831
|
-
/**
|
|
16832
|
-
* The `HooksConfig` key whose `hooks` block provides tool-specific overrides
|
|
16833
|
-
* for this target. The legacy `kiro` alias uses `kiro`; {@link import(
|
|
16834
|
-
* "./kiro-cli-hooks.js").KiroCliHooks} overrides this to `kiro-cli`.
|
|
16835
|
-
*/
|
|
16836
|
-
static getOverrideKey() {
|
|
16837
|
-
return "kiro";
|
|
16838
|
-
}
|
|
16839
17756
|
isDeletable() {
|
|
16840
17757
|
return false;
|
|
16841
17758
|
}
|
|
@@ -16860,7 +17777,7 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16860
17777
|
const paths = KiroHooks.getSettablePaths({ global });
|
|
16861
17778
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
16862
17779
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
16863
|
-
const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson()
|
|
17780
|
+
const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson());
|
|
16864
17781
|
const fileContent = applySharedConfigPatch({
|
|
16865
17782
|
fileKey: sharedConfigFileKey(paths),
|
|
16866
17783
|
feature: "hooks",
|
|
@@ -16884,10 +17801,9 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16884
17801
|
throw new Error(`Failed to parse Kiro hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
16885
17802
|
}
|
|
16886
17803
|
const hooks = kiroHooksToCanonical(agentConfig.hooks);
|
|
16887
|
-
const overrideKey = this.constructor.getOverrideKey();
|
|
16888
17804
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
16889
17805
|
hooks,
|
|
16890
|
-
overrideKey
|
|
17806
|
+
overrideKey: "kiro"
|
|
16891
17807
|
}), null, 2) });
|
|
16892
17808
|
}
|
|
16893
17809
|
validate() {
|
|
@@ -16907,222 +17823,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16907
17823
|
}
|
|
16908
17824
|
};
|
|
16909
17825
|
//#endregion
|
|
16910
|
-
//#region src/features/hooks/kiro-cli-hooks.ts
|
|
16911
|
-
/**
|
|
16912
|
-
* Hooks generator for the **Kiro CLI**.
|
|
16913
|
-
*
|
|
16914
|
-
* The Kiro CLI uses the same `.kiro/agents/default.json` agent-hook format as
|
|
16915
|
-
* the legacy `kiro` alias, so this reuses {@link KiroHooks} and only redirects
|
|
16916
|
-
* the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
16917
|
-
* the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
|
|
16918
|
-
*
|
|
16919
|
-
* (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
|
|
16920
|
-
* {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
|
|
16921
|
-
*/
|
|
16922
|
-
var KiroCliHooks = class extends KiroHooks {
|
|
16923
|
-
static getOverrideKey() {
|
|
16924
|
-
return "kiro-cli";
|
|
16925
|
-
}
|
|
16926
|
-
};
|
|
16927
|
-
//#endregion
|
|
16928
|
-
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
16929
|
-
/**
|
|
16930
|
-
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
16931
|
-
*
|
|
16932
|
-
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
16933
|
-
* imports do not drop data they do not yet understand.
|
|
16934
|
-
* @see https://kiro.dev/docs/hooks/types/
|
|
16935
|
-
*/
|
|
16936
|
-
const KiroIdeHookActionSchema = zod_mini.z.union([zod_mini.z.looseObject({
|
|
16937
|
-
type: zod_mini.z.literal("command"),
|
|
16938
|
-
command: zod_mini.z.optional(safeString)
|
|
16939
|
-
}), zod_mini.z.looseObject({
|
|
16940
|
-
type: zod_mini.z.literal("agent"),
|
|
16941
|
-
prompt: zod_mini.z.optional(safeString)
|
|
16942
|
-
})]);
|
|
16943
|
-
const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
|
|
16944
|
-
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
16945
|
-
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
16946
|
-
trigger: zod_mini.z.optional(zod_mini.z.string()),
|
|
16947
|
-
matcher: zod_mini.z.optional(zod_mini.z.string()),
|
|
16948
|
-
action: zod_mini.z.optional(KiroIdeHookActionSchema),
|
|
16949
|
-
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
16950
|
-
enabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
16951
|
-
});
|
|
16952
|
-
const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
|
|
16953
|
-
version: zod_mini.z.optional(zod_mini.z.string()),
|
|
16954
|
-
hooks: zod_mini.z.optional(zod_mini.z.array(KiroIdeHookEntrySchema))
|
|
16955
|
-
});
|
|
16956
|
-
/**
|
|
16957
|
-
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
16958
|
-
*
|
|
16959
|
-
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
16960
|
-
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
16961
|
-
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
16962
|
-
* already warns about unsupported types).
|
|
16963
|
-
*/
|
|
16964
|
-
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
16965
|
-
const entries = [];
|
|
16966
|
-
for (const def of definitions) {
|
|
16967
|
-
const type = def.type ?? "command";
|
|
16968
|
-
let action;
|
|
16969
|
-
if (type === "command") {
|
|
16970
|
-
if (def.command === void 0) continue;
|
|
16971
|
-
action = {
|
|
16972
|
-
type: "command",
|
|
16973
|
-
command: def.command
|
|
16974
|
-
};
|
|
16975
|
-
} else if (type === "prompt") {
|
|
16976
|
-
if (def.prompt === void 0) continue;
|
|
16977
|
-
action = {
|
|
16978
|
-
type: "agent",
|
|
16979
|
-
prompt: def.prompt
|
|
16980
|
-
};
|
|
16981
|
-
} else continue;
|
|
16982
|
-
entries.push({
|
|
16983
|
-
name: def.name ?? trigger,
|
|
16984
|
-
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
16985
|
-
trigger,
|
|
16986
|
-
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
16987
|
-
action,
|
|
16988
|
-
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
16989
|
-
enabled: def.enabled ?? true
|
|
16990
|
-
});
|
|
16991
|
-
}
|
|
16992
|
-
return entries;
|
|
16993
|
-
}
|
|
16994
|
-
function canonicalToKiroIdeHooks(config) {
|
|
16995
|
-
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
16996
|
-
const sharedHooks = {};
|
|
16997
|
-
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
16998
|
-
const effectiveHooks = {
|
|
16999
|
-
...sharedHooks,
|
|
17000
|
-
...config["kiro-ide"]?.hooks
|
|
17001
|
-
};
|
|
17002
|
-
const entries = [];
|
|
17003
|
-
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
17004
|
-
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
17005
|
-
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
17006
|
-
}
|
|
17007
|
-
return entries;
|
|
17008
|
-
}
|
|
17009
|
-
function kiroIdeHooksToCanonical(entries) {
|
|
17010
|
-
const canonical = {};
|
|
17011
|
-
for (const entry of entries) {
|
|
17012
|
-
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
17013
|
-
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
17014
|
-
if (isPrototypePollutionKey(eventName)) continue;
|
|
17015
|
-
const def = {};
|
|
17016
|
-
if (entry.action.type === "command") {
|
|
17017
|
-
if (!entry.action.command) continue;
|
|
17018
|
-
def.type = "command";
|
|
17019
|
-
def.command = entry.action.command;
|
|
17020
|
-
} else {
|
|
17021
|
-
if (!entry.action.prompt) continue;
|
|
17022
|
-
def.type = "prompt";
|
|
17023
|
-
def.prompt = entry.action.prompt;
|
|
17024
|
-
}
|
|
17025
|
-
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
17026
|
-
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
17027
|
-
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
17028
|
-
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
17029
|
-
if (entry.enabled === false) def.enabled = false;
|
|
17030
|
-
(canonical[eventName] ??= []).push(def);
|
|
17031
|
-
}
|
|
17032
|
-
return canonical;
|
|
17033
|
-
}
|
|
17034
|
-
/**
|
|
17035
|
-
* Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
|
|
17036
|
-
*
|
|
17037
|
-
* Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
17038
|
-
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
17039
|
-
* `hooks` array, so rulesync emits every generated hook into one
|
|
17040
|
-
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
17041
|
-
* which keeps it within the single-file hooks architecture.
|
|
17042
|
-
*
|
|
17043
|
-
* This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
|
|
17044
|
-
* KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
|
|
17045
|
-
*
|
|
17046
|
-
* @see https://kiro.dev/docs/hooks/
|
|
17047
|
-
*/
|
|
17048
|
-
var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
|
|
17049
|
-
constructor(params) {
|
|
17050
|
-
super({
|
|
17051
|
-
...params,
|
|
17052
|
-
fileContent: params.fileContent ?? JSON.stringify({
|
|
17053
|
-
version: "v1",
|
|
17054
|
-
hooks: []
|
|
17055
|
-
}, null, 2)
|
|
17056
|
-
});
|
|
17057
|
-
}
|
|
17058
|
-
static getSettablePaths(_options = {}) {
|
|
17059
|
-
return {
|
|
17060
|
-
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
17061
|
-
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
17062
|
-
};
|
|
17063
|
-
}
|
|
17064
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
17065
|
-
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
17066
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
17067
|
-
version: "v1",
|
|
17068
|
-
hooks: []
|
|
17069
|
-
}, null, 2);
|
|
17070
|
-
return new KiroIdeHooks({
|
|
17071
|
-
outputRoot,
|
|
17072
|
-
relativeDirPath: paths.relativeDirPath,
|
|
17073
|
-
relativeFilePath: paths.relativeFilePath,
|
|
17074
|
-
fileContent,
|
|
17075
|
-
validate
|
|
17076
|
-
});
|
|
17077
|
-
}
|
|
17078
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
17079
|
-
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
17080
|
-
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
|
|
17081
|
-
const fileContent = JSON.stringify({
|
|
17082
|
-
version: "v1",
|
|
17083
|
-
hooks
|
|
17084
|
-
}, null, 2);
|
|
17085
|
-
return new KiroIdeHooks({
|
|
17086
|
-
outputRoot,
|
|
17087
|
-
relativeDirPath: paths.relativeDirPath,
|
|
17088
|
-
relativeFilePath: paths.relativeFilePath,
|
|
17089
|
-
fileContent,
|
|
17090
|
-
validate
|
|
17091
|
-
});
|
|
17092
|
-
}
|
|
17093
|
-
toRulesyncHooks() {
|
|
17094
|
-
let parsed;
|
|
17095
|
-
try {
|
|
17096
|
-
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
17097
|
-
} catch (error) {
|
|
17098
|
-
throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17099
|
-
}
|
|
17100
|
-
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
17101
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
17102
|
-
hooks,
|
|
17103
|
-
overrideKey: "kiro-ide"
|
|
17104
|
-
}), null, 2) });
|
|
17105
|
-
}
|
|
17106
|
-
validate() {
|
|
17107
|
-
return {
|
|
17108
|
-
success: true,
|
|
17109
|
-
error: null
|
|
17110
|
-
};
|
|
17111
|
-
}
|
|
17112
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
17113
|
-
return new KiroIdeHooks({
|
|
17114
|
-
outputRoot,
|
|
17115
|
-
relativeDirPath,
|
|
17116
|
-
relativeFilePath,
|
|
17117
|
-
fileContent: JSON.stringify({
|
|
17118
|
-
version: "v1",
|
|
17119
|
-
hooks: []
|
|
17120
|
-
}, null, 2),
|
|
17121
|
-
validate: false
|
|
17122
|
-
});
|
|
17123
|
-
}
|
|
17124
|
-
};
|
|
17125
|
-
//#endregion
|
|
17126
17826
|
//#region src/features/hooks/opencode-hooks.ts
|
|
17127
17827
|
var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
17128
17828
|
constructor(params) {
|
|
@@ -18186,6 +18886,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
18186
18886
|
supportedHookTypes: ["command"],
|
|
18187
18887
|
supportsMatcher: true
|
|
18188
18888
|
}],
|
|
18889
|
+
["cline", {
|
|
18890
|
+
class: ClineHooks,
|
|
18891
|
+
meta: {
|
|
18892
|
+
supportsProject: true,
|
|
18893
|
+
supportsGlobal: true,
|
|
18894
|
+
supportsImport: false
|
|
18895
|
+
},
|
|
18896
|
+
supportedEvents: CLINE_HOOK_EVENTS,
|
|
18897
|
+
supportedHookTypes: ["command"],
|
|
18898
|
+
supportsMatcher: false
|
|
18899
|
+
}],
|
|
18189
18900
|
["goose", {
|
|
18190
18901
|
class: GooseHooks,
|
|
18191
18902
|
meta: {
|
|
@@ -18247,12 +18958,13 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
18247
18958
|
class: KiroCliHooks,
|
|
18248
18959
|
meta: {
|
|
18249
18960
|
supportsProject: true,
|
|
18250
|
-
supportsGlobal:
|
|
18961
|
+
supportsGlobal: true,
|
|
18251
18962
|
supportsImport: true
|
|
18252
18963
|
},
|
|
18253
|
-
supportedEvents:
|
|
18254
|
-
supportedHookTypes: ["command"],
|
|
18255
|
-
supportsMatcher: true
|
|
18964
|
+
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
18965
|
+
supportedHookTypes: ["command", "prompt"],
|
|
18966
|
+
supportsMatcher: true,
|
|
18967
|
+
passthroughOverrideEvents: true
|
|
18256
18968
|
}],
|
|
18257
18969
|
["kiro-ide", {
|
|
18258
18970
|
class: KiroIdeHooks,
|
|
@@ -18391,7 +19103,11 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
18391
19103
|
relativeFilePath: paths.relativeFilePath,
|
|
18392
19104
|
global: this.global
|
|
18393
19105
|
});
|
|
18394
|
-
const
|
|
19106
|
+
const auxiliaryFiles = await factory.class.getDeletableAuxiliaryFiles?.({
|
|
19107
|
+
outputRoot: this.outputRoot,
|
|
19108
|
+
global: this.global
|
|
19109
|
+
}) ?? [];
|
|
19110
|
+
const list = [...toolHooks.isDeletable?.() !== false ? [toolHooks] : [], ...auxiliaryFiles.filter((file) => file.isDeletable())];
|
|
18395
19111
|
this.logger.debug(`Successfully loaded ${list.length} ${this.toolTarget} hooks files for deletion`);
|
|
18396
19112
|
return list;
|
|
18397
19113
|
}
|
|
@@ -18456,7 +19172,7 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
18456
19172
|
effectiveHooks
|
|
18457
19173
|
});
|
|
18458
19174
|
if (eventsWithUnsupportedMatcher.length > 0) this.logger.warn(`Skipped matcher hook(s) for ${this.toolTarget} (not supported): ${eventsWithUnsupportedMatcher.join(", ")}`);
|
|
18459
|
-
const
|
|
19175
|
+
const toolHooks = await factory.class.fromRulesyncHooks({
|
|
18460
19176
|
outputRoot: this.outputRoot,
|
|
18461
19177
|
rulesyncHooks,
|
|
18462
19178
|
validate: true,
|
|
@@ -18465,10 +19181,16 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
18465
19181
|
logger: this.logger,
|
|
18466
19182
|
toolTarget: this.toolTarget
|
|
18467
19183
|
})
|
|
18468
|
-
})
|
|
19184
|
+
});
|
|
19185
|
+
const result = [toolHooks];
|
|
18469
19186
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
18470
19187
|
outputRoot: this.outputRoot,
|
|
18471
|
-
global: this.global
|
|
19188
|
+
global: this.global,
|
|
19189
|
+
toolHooks,
|
|
19190
|
+
logger: withToolTargetPrefix({
|
|
19191
|
+
logger: this.logger,
|
|
19192
|
+
toolTarget: this.toolTarget
|
|
19193
|
+
})
|
|
18472
19194
|
});
|
|
18473
19195
|
if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
|
|
18474
19196
|
return result;
|
|
@@ -18946,29 +19668,43 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
|
|
|
18946
19668
|
* `.codeiumignore` filename is read as a fallback so existing projects still
|
|
18947
19669
|
* round-trip.
|
|
18948
19670
|
*
|
|
19671
|
+
* In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
|
|
19672
|
+
* instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
|
|
19673
|
+
* legacy brand spelling and sits outside `~/.config/devin`.
|
|
19674
|
+
*
|
|
18949
19675
|
* @see https://docs.devin.ai/desktop/changelog — v3.1.7 added `.devinignore`
|
|
18950
19676
|
* alongside `.windsurfignore` and `.codeiumignore`.
|
|
18951
19677
|
*/
|
|
18952
19678
|
var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
18953
|
-
static getSettablePaths() {
|
|
19679
|
+
static getSettablePaths({ global = false } = {}) {
|
|
18954
19680
|
return {
|
|
18955
|
-
relativeDirPath: ".",
|
|
18956
|
-
relativeFilePath: DEVIN_IGNORE_FILE_NAME
|
|
19681
|
+
relativeDirPath: global ? DEVIN_GLOBAL_IGNORE_DIR_PATH : ".",
|
|
19682
|
+
relativeFilePath: global ? DEVIN_GLOBAL_IGNORE_FILE_NAME : DEVIN_IGNORE_FILE_NAME
|
|
18957
19683
|
};
|
|
18958
19684
|
}
|
|
18959
19685
|
toRulesyncIgnore() {
|
|
18960
19686
|
return this.toRulesyncIgnoreDefault();
|
|
18961
19687
|
}
|
|
18962
|
-
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
19688
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
19689
|
+
const paths = this.getSettablePaths({ global });
|
|
18963
19690
|
return new DevinIgnore({
|
|
18964
19691
|
outputRoot,
|
|
18965
|
-
relativeDirPath:
|
|
18966
|
-
relativeFilePath:
|
|
18967
|
-
fileContent: rulesyncIgnore.getFileContent()
|
|
19692
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19693
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19694
|
+
fileContent: rulesyncIgnore.getFileContent(),
|
|
19695
|
+
global
|
|
18968
19696
|
});
|
|
18969
19697
|
}
|
|
18970
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
18971
|
-
const { relativeDirPath, relativeFilePath } = this.getSettablePaths();
|
|
19698
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
19699
|
+
const { relativeDirPath, relativeFilePath } = this.getSettablePaths({ global });
|
|
19700
|
+
if (global) return new DevinIgnore({
|
|
19701
|
+
outputRoot,
|
|
19702
|
+
relativeDirPath,
|
|
19703
|
+
relativeFilePath,
|
|
19704
|
+
fileContent: await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath)),
|
|
19705
|
+
validate,
|
|
19706
|
+
global
|
|
19707
|
+
});
|
|
18972
19708
|
const primaryPath = (0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath);
|
|
18973
19709
|
const legacyPath = (0, node_path.join)(outputRoot, relativeDirPath, DEVIN_LEGACY_IGNORE_FILE_NAME);
|
|
18974
19710
|
const resolvedFilePath = !await fileExists(primaryPath) && await fileExists(legacyPath) ? DEVIN_LEGACY_IGNORE_FILE_NAME : relativeFilePath;
|
|
@@ -18978,16 +19714,18 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
18978
19714
|
relativeDirPath,
|
|
18979
19715
|
relativeFilePath: resolvedFilePath,
|
|
18980
19716
|
fileContent,
|
|
18981
|
-
validate
|
|
19717
|
+
validate,
|
|
19718
|
+
global
|
|
18982
19719
|
});
|
|
18983
19720
|
}
|
|
18984
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
19721
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
18985
19722
|
return new DevinIgnore({
|
|
18986
19723
|
outputRoot,
|
|
18987
19724
|
relativeDirPath,
|
|
18988
19725
|
relativeFilePath,
|
|
18989
19726
|
fileContent: "",
|
|
18990
|
-
validate: false
|
|
19727
|
+
validate: false,
|
|
19728
|
+
global
|
|
18991
19729
|
});
|
|
18992
19730
|
}
|
|
18993
19731
|
};
|
|
@@ -19861,6 +20599,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
19861
20599
|
]);
|
|
19862
20600
|
const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
|
|
19863
20601
|
const ignoreProcessorGlobalToolTargets = [
|
|
20602
|
+
"devin",
|
|
19864
20603
|
"kiro",
|
|
19865
20604
|
"kiro-cli",
|
|
19866
20605
|
"kiro-ide",
|
|
@@ -20788,6 +21527,65 @@ const RULESYNC_TO_CODEX_SCALAR_FIELD_MAP = { experimentalEnvironment: "experimen
|
|
|
20788
21527
|
const CODEX_TO_RULESYNC_SCALAR_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
|
|
20789
21528
|
const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
20790
21529
|
/**
|
|
21530
|
+
* Canonical per-server keys Codex has no counterpart for.
|
|
21531
|
+
*
|
|
21532
|
+
* Codex's deserializer (`RawMcpServerConfig`) does not reject unknown keys, so
|
|
21533
|
+
* these are inert rather than fatal — but they are rulesync's own spellings and
|
|
21534
|
+
* only add noise to a hand-edited `config.toml`. `type`/`transport` are safe to
|
|
21535
|
+
* drop because Codex infers the transport from `command` versus `url`.
|
|
21536
|
+
* `tools` is handled separately: it is fatal rather than inert.
|
|
21537
|
+
* @see https://github.com/openai/codex/blob/rust-v0.146.1/codex-rs/config/src/mcp_types.rs
|
|
21538
|
+
*/
|
|
21539
|
+
const CODEX_UNSUPPORTED_CANONICAL_KEYS = /* @__PURE__ */ new Set([
|
|
21540
|
+
"type",
|
|
21541
|
+
"transport",
|
|
21542
|
+
"alwaysAllow",
|
|
21543
|
+
"trust",
|
|
21544
|
+
"kiroAutoApprove",
|
|
21545
|
+
"kiroAutoBlock"
|
|
21546
|
+
]);
|
|
21547
|
+
/**
|
|
21548
|
+
* Canonical millisecond timeouts and the Codex fields they translate to.
|
|
21549
|
+
* Codex takes both as seconds (`f64`), so the value is divided by 1000 and a
|
|
21550
|
+
* fractional result is emitted as-is.
|
|
21551
|
+
* - `timeout` → `tool_timeout_sec`: default timeout for tool calls on the server.
|
|
21552
|
+
* - `networkTimeout` → `startup_timeout_sec`: initialize + list-tools timeout.
|
|
21553
|
+
*/
|
|
21554
|
+
const RULESYNC_TO_CODEX_TIMEOUT_FIELD_MAP = {
|
|
21555
|
+
timeout: "tool_timeout_sec",
|
|
21556
|
+
networkTimeout: "startup_timeout_sec"
|
|
21557
|
+
};
|
|
21558
|
+
const CODEX_TO_RULESYNC_TIMEOUT_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_TIMEOUT_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
|
|
21559
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
21560
|
+
/**
|
|
21561
|
+
* Whether a value is usable as a timeout. Codex builds a `Duration` out of both
|
|
21562
|
+
* timeout fields, and `Duration::try_from_secs_f64` errors on a negative value —
|
|
21563
|
+
* which fails the whole `config.toml`, not just the one server — so a negative
|
|
21564
|
+
* timeout is rejected here rather than written.
|
|
21565
|
+
*/
|
|
21566
|
+
function isTimeoutValue(value) {
|
|
21567
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
21568
|
+
}
|
|
21569
|
+
/**
|
|
21570
|
+
* Whether a value is usable as the canonical `headers` map, whose schema is
|
|
21571
|
+
* `record(string, string)`. Checked in both directions so a hand-written
|
|
21572
|
+
* `http_headers` can never be imported into a `.rulesync/mcp.jsonc` that the
|
|
21573
|
+
* next generate would refuse to parse.
|
|
21574
|
+
*/
|
|
21575
|
+
function isHeadersRecord(value) {
|
|
21576
|
+
return isPlainObject$1(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
21577
|
+
}
|
|
21578
|
+
/**
|
|
21579
|
+
* Whether a server config describes a Codex stdio server. Codex branches on
|
|
21580
|
+
* `command` first; a config carrying both `command` and `url` is classified as
|
|
21581
|
+
* stdio here, which matches upstream in the sense that it never reaches the
|
|
21582
|
+
* remote arm — upstream rejects that combination outright ("url is not
|
|
21583
|
+
* supported for stdio").
|
|
21584
|
+
*/
|
|
21585
|
+
function isCodexStdioServer(config) {
|
|
21586
|
+
return config["command"] !== void 0;
|
|
21587
|
+
}
|
|
21588
|
+
/**
|
|
20791
21589
|
* `env_vars` entries are either a bare variable name or `{ name, source }`,
|
|
20792
21590
|
* where `source = "remote"` reads the variable from the remote executor
|
|
20793
21591
|
* environment. The other renamed keys (`enabled_tools`, `disabled_tools`) stay
|
|
@@ -20845,6 +21643,75 @@ function normalizeCodexMcpServerName(name) {
|
|
|
20845
21643
|
usedFallback: true
|
|
20846
21644
|
};
|
|
20847
21645
|
}
|
|
21646
|
+
/**
|
|
21647
|
+
* Translate the Codex-native per-server keys that carry a canonical
|
|
21648
|
+
* counterpart under a different name or unit. Returns `undefined` for a key
|
|
21649
|
+
* this translation does not own, leaving it to the caller's other branches.
|
|
21650
|
+
*/
|
|
21651
|
+
function translateCodexOnlyKey({ key, value, config, serverName }) {
|
|
21652
|
+
if (key === "tools") return {};
|
|
21653
|
+
if (key === "http_headers") {
|
|
21654
|
+
if ("headers" in config) return {};
|
|
21655
|
+
if (isHeadersRecord(value)) return { entry: ["headers", omitPrototypePollutionKeys(value)] };
|
|
21656
|
+
warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${serverName}: expected a table of string values`);
|
|
21657
|
+
return {};
|
|
21658
|
+
}
|
|
21659
|
+
const mappedKey = CODEX_TO_RULESYNC_TIMEOUT_FIELD_MAP[key];
|
|
21660
|
+
if (mappedKey) {
|
|
21661
|
+
if (isTimeoutValue(value)) return { entry: [mappedKey, value * MILLISECONDS_PER_SECOND] };
|
|
21662
|
+
warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${serverName}: expected a non-negative number of seconds`);
|
|
21663
|
+
return {};
|
|
21664
|
+
}
|
|
21665
|
+
if (key === "startup_timeout_ms") {
|
|
21666
|
+
if ("startup_timeout_sec" in config) return {};
|
|
21667
|
+
if (isTimeoutValue(value)) return { entry: ["networkTimeout", value] };
|
|
21668
|
+
warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${serverName}: expected a non-negative number of milliseconds`);
|
|
21669
|
+
return {};
|
|
21670
|
+
}
|
|
21671
|
+
}
|
|
21672
|
+
/**
|
|
21673
|
+
* Translate the canonical per-server keys that Codex spells differently, reads
|
|
21674
|
+
* in another unit, or cannot accept at all. Returns `undefined` for a key this
|
|
21675
|
+
* translation does not own.
|
|
21676
|
+
*/
|
|
21677
|
+
function translateCanonicalKeyToCodex({ key, value, isStdio, serverName }) {
|
|
21678
|
+
if (key === "tools") {
|
|
21679
|
+
warnWithFallback(void 0, `[CodexCliMcp] Dropping 'tools' from MCP server "${serverName}": Codex reads it as a per-tool approval table, not a tool allowlist. Use 'enabledTools' / 'disabledTools' instead.`);
|
|
21680
|
+
return {};
|
|
21681
|
+
}
|
|
21682
|
+
if (CODEX_UNSUPPORTED_CANONICAL_KEYS.has(key)) return {};
|
|
21683
|
+
if (key === "headers") {
|
|
21684
|
+
if (!isHeadersRecord(value)) {
|
|
21685
|
+
warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key 'headers': expected a table of string values, got ${typeof value}`);
|
|
21686
|
+
return {};
|
|
21687
|
+
}
|
|
21688
|
+
if (isStdio) {
|
|
21689
|
+
warnWithFallback(void 0, `[CodexCliMcp] Dropping 'headers' from stdio MCP server "${serverName}": Codex accepts HTTP headers only on url-based servers.`);
|
|
21690
|
+
return {};
|
|
21691
|
+
}
|
|
21692
|
+
return { entry: ["http_headers", omitPrototypePollutionKeys(value)] };
|
|
21693
|
+
}
|
|
21694
|
+
const mappedKey = RULESYNC_TO_CODEX_TIMEOUT_FIELD_MAP[key];
|
|
21695
|
+
if (mappedKey) {
|
|
21696
|
+
if (isTimeoutValue(value)) return { entry: [mappedKey, value / MILLISECONDS_PER_SECOND] };
|
|
21697
|
+
warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected a non-negative number of milliseconds, got ${typeof value}`);
|
|
21698
|
+
return {};
|
|
21699
|
+
}
|
|
21700
|
+
}
|
|
21701
|
+
/**
|
|
21702
|
+
* Codex states no transport of its own — it infers one from `command` versus
|
|
21703
|
+
* `url`, which is why generate drops the canonical `type`. Restate it for a url
|
|
21704
|
+
* server on the way back, so a config imported from Codex reaches the adapters
|
|
21705
|
+
* that branch on `type` as a remote server rather than one with no transport at
|
|
21706
|
+
* all. `streamable_http` is Codex's only remote transport, and canonical spells
|
|
21707
|
+
* that `http`.
|
|
21708
|
+
*/
|
|
21709
|
+
function restateCanonicalTransport(converted) {
|
|
21710
|
+
if (converted["type"] !== void 0) return;
|
|
21711
|
+
if (isCodexStdioServer(converted)) return;
|
|
21712
|
+
if (typeof converted["url"] !== "string") return;
|
|
21713
|
+
converted["type"] = "http";
|
|
21714
|
+
}
|
|
20848
21715
|
function convertFromCodexFormat(codexMcp) {
|
|
20849
21716
|
const result = {};
|
|
20850
21717
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
@@ -20852,7 +21719,15 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
20852
21719
|
const converted = {};
|
|
20853
21720
|
for (const [key, value] of Object.entries(config)) {
|
|
20854
21721
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20855
|
-
|
|
21722
|
+
const codexOnly = translateCodexOnlyKey({
|
|
21723
|
+
key,
|
|
21724
|
+
value,
|
|
21725
|
+
config,
|
|
21726
|
+
serverName: name
|
|
21727
|
+
});
|
|
21728
|
+
if (codexOnly) {
|
|
21729
|
+
if (codexOnly.entry) converted[codexOnly.entry[0]] = codexOnly.entry[1];
|
|
21730
|
+
} else if (key === "enabled") {
|
|
20856
21731
|
if (value === false) converted["disabled"] = true;
|
|
20857
21732
|
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
|
|
20858
21733
|
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
@@ -20865,6 +21740,7 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
20865
21740
|
else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
|
|
20866
21741
|
} else converted[key] = value;
|
|
20867
21742
|
}
|
|
21743
|
+
restateCanonicalTransport(converted);
|
|
20868
21744
|
result[name] = converted;
|
|
20869
21745
|
}
|
|
20870
21746
|
return result;
|
|
@@ -20877,9 +21753,18 @@ function convertToCodexFormat(mcpServers) {
|
|
|
20877
21753
|
const { codexName, usedFallback } = normalizeCodexMcpServerName(name);
|
|
20878
21754
|
if (usedFallback) warnWithFallback(void 0, `MCP server "${name}" cannot be represented as a Codex MCP server name (ASCII [a-zA-Z0-9_-] only), so the stable fallback name "${codexName}" was used. Rename the server in .rulesync/mcp.jsonc to choose a readable Codex name.`);
|
|
20879
21755
|
const converted = {};
|
|
21756
|
+
const isStdio = isCodexStdioServer(config);
|
|
20880
21757
|
for (const [key, value] of Object.entries(config)) {
|
|
20881
21758
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20882
|
-
|
|
21759
|
+
const translated = translateCanonicalKeyToCodex({
|
|
21760
|
+
key,
|
|
21761
|
+
value,
|
|
21762
|
+
isStdio,
|
|
21763
|
+
serverName: name
|
|
21764
|
+
});
|
|
21765
|
+
if (translated) {
|
|
21766
|
+
if (translated.entry) converted[translated.entry[0]] = translated.entry[1];
|
|
21767
|
+
} else if (key === "disabled") {
|
|
20883
21768
|
if (value === true) converted["enabled"] = false;
|
|
20884
21769
|
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
|
|
20885
21770
|
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
@@ -22322,7 +23207,8 @@ function resolveHermesTimeout(config) {
|
|
|
22322
23207
|
* Copies the advanced Hermes-recognized per-server fields that have no canonical
|
|
22323
23208
|
* alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
|
|
22324
23209
|
* path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
|
|
22325
|
-
* `connect_timeout` (seconds),
|
|
23210
|
+
* `connect_timeout` (seconds), `supports_parallel_tool_calls`,
|
|
23211
|
+
* `keepalive_interval`, and `elicitation` — verbatim
|
|
22326
23212
|
* from `source` to `target`. Field names are identical on both sides (the
|
|
22327
23213
|
* canonical `McpServerSchema` is a `looseObject`), so this serves export and
|
|
22328
23214
|
* import alike. See the Hermes mcp-config-reference.
|
|
@@ -22383,6 +23269,14 @@ function copyHermesAdvancedFields(source, target) {
|
|
|
22383
23269
|
target.sampling = omitPrototypePollutionKeys(structuredClone(source.sampling));
|
|
22384
23270
|
copied = true;
|
|
22385
23271
|
}
|
|
23272
|
+
if (typeof source.keepalive_interval === "number") {
|
|
23273
|
+
target.keepalive_interval = source.keepalive_interval;
|
|
23274
|
+
copied = true;
|
|
23275
|
+
}
|
|
23276
|
+
if (isPlainObject$1(source.elicitation)) {
|
|
23277
|
+
target.elicitation = omitPrototypePollutionKeys(structuredClone(source.elicitation));
|
|
23278
|
+
copied = true;
|
|
23279
|
+
}
|
|
22386
23280
|
return copied;
|
|
22387
23281
|
}
|
|
22388
23282
|
/**
|
|
@@ -22426,8 +23320,10 @@ function applyHermesToolsBlock(hermesTools, server) {
|
|
|
22426
23320
|
* `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
|
|
22427
23321
|
* exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
|
|
22428
23322
|
* fields Hermes understands are emitted, so the shared `config.yaml` is not
|
|
22429
|
-
* polluted with canonical-only aliases (`type`, `
|
|
22430
|
-
*
|
|
23323
|
+
* polluted with canonical-only aliases (`type`, `httpUrl`, `networkTimeout`,
|
|
23324
|
+
* ...) — with one exception since v0.20.0: a canonical `sse` server is written
|
|
23325
|
+
* as Hermes's own `transport: sse`, without which Hermes would connect to it
|
|
23326
|
+
* over Streamable HTTP.
|
|
22431
23327
|
*/
|
|
22432
23328
|
function convertServerToHermes(config) {
|
|
22433
23329
|
const out = {};
|
|
@@ -22447,6 +23343,7 @@ function convertServerToHermes(config) {
|
|
|
22447
23343
|
} else if (url !== void 0) {
|
|
22448
23344
|
out.url = url;
|
|
22449
23345
|
if (isPlainObject$1(config.headers)) out.headers = omitPrototypePollutionKeys(config.headers);
|
|
23346
|
+
if (config.type === "sse" || config.transport === "sse") out.transport = "sse";
|
|
22450
23347
|
}
|
|
22451
23348
|
if (config.disabled === true) out.enabled = false;
|
|
22452
23349
|
const timeout = resolveHermesTimeout(config);
|
|
@@ -22494,6 +23391,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
22494
23391
|
if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
|
|
22495
23392
|
if (typeof config.url === "string") server.url = config.url;
|
|
22496
23393
|
if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
23394
|
+
if (typeof config.url === "string" && config.transport === "sse") server.type = "sse";
|
|
22497
23395
|
if (config.enabled === false) server.disabled = true;
|
|
22498
23396
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
22499
23397
|
if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
@@ -24066,6 +24964,7 @@ const REASONIX_PLUGIN_FIELDS = [
|
|
|
24066
24964
|
"env",
|
|
24067
24965
|
"url",
|
|
24068
24966
|
"headers",
|
|
24967
|
+
"startup_timeout_seconds",
|
|
24069
24968
|
"call_timeout_seconds",
|
|
24070
24969
|
"tool_timeout_seconds"
|
|
24071
24970
|
];
|
|
@@ -24742,20 +25641,6 @@ function deriveTransportAllowlist(servers) {
|
|
|
24742
25641
|
return allowlist;
|
|
24743
25642
|
}
|
|
24744
25643
|
//#endregion
|
|
24745
|
-
//#region src/features/shared/vibe-config-scope.ts
|
|
24746
|
-
/**
|
|
24747
|
-
* Vibe selects exactly **one** persistence TOML and does not merge scopes: the
|
|
24748
|
-
* trusted project `.vibe/config.toml` when one is discovered, otherwise
|
|
24749
|
-
* `~/.vibe/config.toml` (single code path since v2.22.0's ConfigOrchestrator
|
|
24750
|
-
* migration). A `--global` run that writes the home file is therefore inert in
|
|
24751
|
-
* any project that has its own config — worth a heads-up, since the other Vibe
|
|
24752
|
-
* surfaces (rules, hooks, agents, skills) genuinely combine scopes.
|
|
24753
|
-
*/
|
|
24754
|
-
async function warnIfGlobalVibeConfigIsShadowed(logger) {
|
|
24755
|
-
if (!await fileExists((0, node_path.join)(process.cwd(), ".vibe", "config.toml"))) return;
|
|
24756
|
-
logger?.warn("Vibe reads exactly one config.toml (project .vibe/config.toml when present, otherwise ~/.vibe/config.toml — a fallback, not a merge). This project has .vibe/config.toml, so the global file written by --global is ignored here.");
|
|
24757
|
-
}
|
|
24758
|
-
//#endregion
|
|
24759
25644
|
//#region src/features/mcp/vibe-mcp.ts
|
|
24760
25645
|
const VIBE_MCP_SERVER_FIELDS = [
|
|
24761
25646
|
"transport",
|
|
@@ -24824,9 +25709,8 @@ var VibeMcp = class VibeMcp extends ToolMcp {
|
|
|
24824
25709
|
global
|
|
24825
25710
|
});
|
|
24826
25711
|
}
|
|
24827
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true,
|
|
25712
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
24828
25713
|
const paths = this.getSettablePaths({ global });
|
|
24829
|
-
if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
|
|
24830
25714
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
24831
25715
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
24832
25716
|
const existingServers = normalizeMcpServersArray(parseSharedConfig({
|
|
@@ -28481,6 +29365,81 @@ function asCursorPermissionEntryArray(value, logger, fieldLabel) {
|
|
|
28481
29365
|
else logger?.warn(`Cursor CLI permissions${fieldLabel ? `.${fieldLabel}` : ""} contains a non-string entry; dropping ${JSON.stringify(item)}.`);
|
|
28482
29366
|
return result;
|
|
28483
29367
|
}
|
|
29368
|
+
/**
|
|
29369
|
+
* Assemble the Cursor CLI config to write.
|
|
29370
|
+
*
|
|
29371
|
+
* Cursor scopes the file asymmetrically: "Only permissions can be configured at
|
|
29372
|
+
* the project level. All other CLI settings must be set globally." So
|
|
29373
|
+
* `version`, `editor.vimMode`, `approvalMode` and `sandbox` belong to
|
|
29374
|
+
* `~/.cursor/cli-config.json` alone — writing them into `.cursor/cli.json`
|
|
29375
|
+
* produces keys Cursor ignores, which silently strands an authored
|
|
29376
|
+
* `cursor.approvalMode`.
|
|
29377
|
+
*
|
|
29378
|
+
* @see https://cursor.com/docs/cli/reference/configuration
|
|
29379
|
+
*/
|
|
29380
|
+
function mergeCursorCliConfig({ settings, mergedPermissions, cursorOverride, global, filePath, logger }) {
|
|
29381
|
+
if (!global) {
|
|
29382
|
+
warnAboutGlobalOnlyOverrideKeys({
|
|
29383
|
+
cursorOverride,
|
|
29384
|
+
filePath,
|
|
29385
|
+
logger
|
|
29386
|
+
});
|
|
29387
|
+
return {
|
|
29388
|
+
...settings,
|
|
29389
|
+
permissions: mergedPermissions
|
|
29390
|
+
};
|
|
29391
|
+
}
|
|
29392
|
+
const existingEditor = asExistingEditor({
|
|
29393
|
+
settings,
|
|
29394
|
+
filePath,
|
|
29395
|
+
logger
|
|
29396
|
+
});
|
|
29397
|
+
return {
|
|
29398
|
+
...settings,
|
|
29399
|
+
...cursorOverride,
|
|
29400
|
+
version: settings.version ?? 1,
|
|
29401
|
+
editor: {
|
|
29402
|
+
...existingEditor,
|
|
29403
|
+
vimMode: existingEditor.vimMode ?? false
|
|
29404
|
+
},
|
|
29405
|
+
permissions: mergedPermissions
|
|
29406
|
+
};
|
|
29407
|
+
}
|
|
29408
|
+
/**
|
|
29409
|
+
* The existing `editor` object to merge rulesync's managed `vimMode` into.
|
|
29410
|
+
* Only called in global scope: a project config's `editor` is passed through
|
|
29411
|
+
* untouched, so narrowing it there would warn about a value nothing ignores.
|
|
29412
|
+
*/
|
|
29413
|
+
function asExistingEditor({ settings, filePath, logger }) {
|
|
29414
|
+
const raw = settings.editor;
|
|
29415
|
+
if (raw === void 0) return {};
|
|
29416
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
29417
|
+
logger?.warn(`Cursor CLI config at ${filePath} has a non-object \`editor\` field; ignoring existing editor settings.`);
|
|
29418
|
+
return {};
|
|
29419
|
+
}
|
|
29420
|
+
/**
|
|
29421
|
+
* Override keys that never reach the file from the override in any scope,
|
|
29422
|
+
* because rulesync re-applies its own managed value over them. Naming them in
|
|
29423
|
+
* the project-scope warning would wrongly promise that `--global` makes them
|
|
29424
|
+
* take effect.
|
|
29425
|
+
*/
|
|
29426
|
+
const CURSOR_OVERRIDE_KEYS_ALWAYS_CLOBBERED = /* @__PURE__ */ new Set([
|
|
29427
|
+
"version",
|
|
29428
|
+
"editor",
|
|
29429
|
+
"permissions"
|
|
29430
|
+
]);
|
|
29431
|
+
/**
|
|
29432
|
+
* Name the override keys that would have been written in global scope but are
|
|
29433
|
+
* dropped here, so the user learns the setting did not take effect and how to
|
|
29434
|
+
* make it.
|
|
29435
|
+
*/
|
|
29436
|
+
function warnAboutGlobalOnlyOverrideKeys({ cursorOverride, filePath, logger }) {
|
|
29437
|
+
const strandedKeys = Object.keys(cursorOverride).filter((key) => !CURSOR_OVERRIDE_KEYS_ALWAYS_CLOBBERED.has(key)).toSorted();
|
|
29438
|
+
if (strandedKeys.length === 0) return;
|
|
29439
|
+
const names = strandedKeys.map((key) => `\`${key}\``).join(", ");
|
|
29440
|
+
const [wasWere, itThem] = strandedKeys.length === 1 ? ["was", "it"] : ["were", "them"];
|
|
29441
|
+
logger?.warn(`Cursor applies only \`permissions\` from a project config, so ${names} from the \`cursor\` override ${wasWere} not written to ${filePath}. Generate with --global to set ${itThem} in ~/.cursor/cli-config.json.`);
|
|
29442
|
+
}
|
|
28484
29443
|
var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
28485
29444
|
constructor(params) {
|
|
28486
29445
|
super({
|
|
@@ -28521,14 +29480,6 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
28521
29480
|
const config = rulesyncPermissions.getJson();
|
|
28522
29481
|
const { allow, deny } = convertRulesyncToCursorPermissions(config, logger);
|
|
28523
29482
|
const managedTypes = new Set(Object.keys(config.permission).map((category) => toCursorType(category)));
|
|
28524
|
-
const existingEditorRaw = settings.editor;
|
|
28525
|
-
let existingEditor;
|
|
28526
|
-
if (existingEditorRaw === void 0) existingEditor = {};
|
|
28527
|
-
else if (existingEditorRaw !== null && typeof existingEditorRaw === "object" && !Array.isArray(existingEditorRaw)) existingEditor = existingEditorRaw;
|
|
28528
|
-
else {
|
|
28529
|
-
logger?.warn(`Cursor CLI config at ${filePath} has a non-object \`editor\` field; ignoring existing editor settings.`);
|
|
28530
|
-
existingEditor = {};
|
|
28531
|
-
}
|
|
28532
29483
|
const existingPermissionsRaw = settings.permissions;
|
|
28533
29484
|
let existingPermissions;
|
|
28534
29485
|
if (existingPermissionsRaw === void 0) existingPermissions = {};
|
|
@@ -28545,17 +29496,14 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
28545
29496
|
mergedPermissions.allow = mergedAllow;
|
|
28546
29497
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
28547
29498
|
else delete mergedPermissions.deny;
|
|
28548
|
-
const
|
|
28549
|
-
|
|
28550
|
-
|
|
28551
|
-
|
|
28552
|
-
|
|
28553
|
-
|
|
28554
|
-
|
|
28555
|
-
|
|
28556
|
-
},
|
|
28557
|
-
permissions: mergedPermissions
|
|
28558
|
-
};
|
|
29499
|
+
const merged = mergeCursorCliConfig({
|
|
29500
|
+
settings,
|
|
29501
|
+
mergedPermissions,
|
|
29502
|
+
cursorOverride: config.cursor ?? {},
|
|
29503
|
+
global,
|
|
29504
|
+
filePath,
|
|
29505
|
+
logger
|
|
29506
|
+
});
|
|
28559
29507
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
28560
29508
|
return new CursorPermissions({
|
|
28561
29509
|
outputRoot,
|
|
@@ -29269,14 +30217,16 @@ const CATEGORY_TO_GROK_TOOL = {
|
|
|
29269
30217
|
edit: "Edit",
|
|
29270
30218
|
write: "Edit",
|
|
29271
30219
|
grep: "Grep",
|
|
29272
|
-
webfetch: "WebFetch"
|
|
30220
|
+
webfetch: "WebFetch",
|
|
30221
|
+
websearch: "WebSearch"
|
|
29273
30222
|
};
|
|
29274
30223
|
const GROK_TOOL_TO_CATEGORY = {
|
|
29275
30224
|
Bash: "bash",
|
|
29276
30225
|
Read: "read",
|
|
29277
30226
|
Edit: "edit",
|
|
29278
30227
|
Grep: "grep",
|
|
29279
|
-
WebFetch: "webfetch"
|
|
30228
|
+
WebFetch: "webfetch",
|
|
30229
|
+
WebSearch: "websearch"
|
|
29280
30230
|
};
|
|
29281
30231
|
const GROK_MCP_TOOL = "MCPTool";
|
|
29282
30232
|
/**
|
|
@@ -29335,18 +30285,18 @@ function parseGrokEntry(entry) {
|
|
|
29335
30285
|
*
|
|
29336
30286
|
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
29337
30287
|
* `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
|
|
29338
|
-
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`,
|
|
29339
|
-
* `WebFetch`, evaluated with precedence `deny > ask > allow`
|
|
30288
|
+
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`,
|
|
30289
|
+
* `WebFetch`, and `WebSearch`, evaluated with precedence `deny > ask > allow`
|
|
29340
30290
|
* (https://docs.x.ai/build/settings/reference). rulesync's canonical
|
|
29341
30291
|
* per-category, per-pattern model maps almost 1:1:
|
|
29342
30292
|
* - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
|
|
29343
30293
|
* the matching Grok entry and is bucketed into the `[permission]` array for
|
|
29344
|
-
* that action. `bash|read|edit|grep|webfetch` map to their Grok
|
|
29345
|
-
* `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*`
|
|
29346
|
-
* `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
30294
|
+
* that action. `bash|read|edit|grep|webfetch|websearch` map to their Grok
|
|
30295
|
+
* tool; `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*`
|
|
30296
|
+
* maps to `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
29347
30297
|
* parentheses, so a non-`*` argument pattern on it is not represented).
|
|
29348
|
-
* Categories with no Grok tool (`
|
|
29349
|
-
*
|
|
30298
|
+
* Categories with no Grok tool (`glob`, `notebookedit`, `agent`) are
|
|
30299
|
+
* skipped (with a warning when they carry a `deny` rule, to
|
|
29350
30300
|
* surface the gap). When two canonical rules collapse onto the same Grok
|
|
29351
30301
|
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
29352
30302
|
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
@@ -29373,8 +30323,8 @@ function parseGrokEntry(entry) {
|
|
|
29373
30323
|
* tools it models and (in global scope) the `[ui] permission_mode` value, while
|
|
29374
30324
|
* every other key
|
|
29375
30325
|
* (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
|
|
29376
|
-
* entries for
|
|
29377
|
-
* file is never deleted.
|
|
30326
|
+
* entries for tool prefixes rulesync cannot model (e.g. `any`) are preserved.
|
|
30327
|
+
* The file is never deleted.
|
|
29378
30328
|
*/
|
|
29379
30329
|
var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
29380
30330
|
constructor(params) {
|
|
@@ -29481,7 +30431,7 @@ const ACTION_RANK = {
|
|
|
29481
30431
|
};
|
|
29482
30432
|
/**
|
|
29483
30433
|
* Collect user-authored entries from an existing `[permission]` array whose
|
|
29484
|
-
* tool prefix rulesync cannot model (e.g. `
|
|
30434
|
+
* tool prefix rulesync cannot model (e.g. `any`). Such entries are
|
|
29485
30435
|
* preserved verbatim so replacing the arrays does not silently drop them
|
|
29486
30436
|
* (mirrors the Cursor adapter's preservation of unmanaged types).
|
|
29487
30437
|
*/
|
|
@@ -31217,7 +32167,12 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
31217
32167
|
"disabled",
|
|
31218
32168
|
"visible"
|
|
31219
32169
|
];
|
|
31220
|
-
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
32170
|
+
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
32171
|
+
"folderTrust",
|
|
32172
|
+
"allowedHttpHookUrls",
|
|
32173
|
+
"allowPrivateNetworkHooks"
|
|
32174
|
+
];
|
|
32175
|
+
const QWEN_GLOBAL_ONLY_SECURITY_KEYS = ["allowPrivateNetworkHooks"];
|
|
31221
32176
|
const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
|
|
31222
32177
|
function asPlainRecord(value) {
|
|
31223
32178
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -31229,6 +32184,21 @@ function pickQwenOverrideKeys(group, keys) {
|
|
|
31229
32184
|
for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
|
|
31230
32185
|
return picked;
|
|
31231
32186
|
}
|
|
32187
|
+
/**
|
|
32188
|
+
* Drop the global-only `security` keys from the override when generating project
|
|
32189
|
+
* settings, warning once per dropped key. Only the override copy is filtered, so
|
|
32190
|
+
* a value the user already wrote into the project file stays untouched.
|
|
32191
|
+
*/
|
|
32192
|
+
function scopeOverrideSecurity(overrideSecurity, { global, relativeFilePath, logger }) {
|
|
32193
|
+
const scoped = { ...asPlainRecord(overrideSecurity) };
|
|
32194
|
+
if (global) return scoped;
|
|
32195
|
+
for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
|
|
32196
|
+
if (scoped[key] === void 0) continue;
|
|
32197
|
+
delete scoped[key];
|
|
32198
|
+
logger?.warn(`Qwen permissions: 'security.${key}' is only honored in user/system settings, so it is skipped for the project-scoped ${relativeFilePath}. Author it in the global scope instead.`);
|
|
32199
|
+
}
|
|
32200
|
+
return scoped;
|
|
32201
|
+
}
|
|
31232
32202
|
var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
31233
32203
|
constructor(params) {
|
|
31234
32204
|
super({
|
|
@@ -31297,10 +32267,18 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
31297
32267
|
...asPlainRecord(settings.tools),
|
|
31298
32268
|
...asPlainRecord(override.tools)
|
|
31299
32269
|
};
|
|
31300
|
-
if (override?.security !== void 0)
|
|
31301
|
-
|
|
31302
|
-
|
|
31303
|
-
|
|
32270
|
+
if (override?.security !== void 0) {
|
|
32271
|
+
const scopedSecurity = scopeOverrideSecurity(override.security, {
|
|
32272
|
+
global,
|
|
32273
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32274
|
+
logger
|
|
32275
|
+
});
|
|
32276
|
+
const mergedSecurity = {
|
|
32277
|
+
...asPlainRecord(settings.security),
|
|
32278
|
+
...scopedSecurity
|
|
32279
|
+
};
|
|
32280
|
+
if (Object.keys(mergedSecurity).length > 0) patch.security = mergedSecurity;
|
|
32281
|
+
}
|
|
31304
32282
|
const fileContent = applySharedConfigPatch({
|
|
31305
32283
|
fileKey: sharedConfigFileKey(paths),
|
|
31306
32284
|
feature: "permissions",
|
|
@@ -31334,6 +32312,10 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
31334
32312
|
});
|
|
31335
32313
|
const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
|
|
31336
32314
|
const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
|
|
32315
|
+
for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
|
|
32316
|
+
if (overrideSecurity[key] === void 0) continue;
|
|
32317
|
+
moduleLogger.warn(`Qwen permissions: imported 'security.${key}'. Qwen Code ignores it in workspace settings but enforces it in user/system settings, so review it before generating with the global scope.`);
|
|
32318
|
+
}
|
|
31337
32319
|
const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
|
|
31338
32320
|
const qwencodeOverride = {};
|
|
31339
32321
|
if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
|
|
@@ -32509,7 +33491,6 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
32509
33491
|
}
|
|
32510
33492
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
32511
33493
|
const paths = this.getSettablePaths({ global });
|
|
32512
|
-
if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
|
|
32513
33494
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
32514
33495
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
32515
33496
|
const config = parseVibeConfig(existingContent);
|
|
@@ -32791,7 +33772,9 @@ function warpSettingsDir() {
|
|
|
32791
33772
|
* `[agents.execution_profiles.<id>]` collection:
|
|
32792
33773
|
* - `command_allowlist` — commands that auto-execute.
|
|
32793
33774
|
* - `command_denylist` — commands that always require permission (the denylist
|
|
32794
|
-
* wins over the allowlist).
|
|
33775
|
+
* wins over the allowlist). Writing it at all replaces Warp's built-in
|
|
33776
|
+
* default denylist, so rulesync warns whenever it emits a non-empty one.
|
|
33777
|
+
* https://docs.warp.dev/cli/permissions-and-profiles/
|
|
32795
33778
|
*
|
|
32796
33779
|
* The legacy `[agents.profiles]` keys
|
|
32797
33780
|
* (`agent_mode_command_execution_allowlist` / `denylist`) are consumed only
|
|
@@ -32889,6 +33872,7 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32889
33872
|
if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
|
|
32890
33873
|
else delete profiles[DENYLIST_KEY];
|
|
32891
33874
|
agents.profiles = profiles;
|
|
33875
|
+
if (mergedDeny.length > 0 && logger) logger.warn(`Warp's command_denylist replaces its built-in default denylist, which covers rm, curl, wget, eval, ssh, shells, and other risky command patterns. The ${mergedDeny.length} deny rule(s) from .rulesync/permissions.jsonc are now the whole denylist — add equivalents for the built-in patterns you want to keep.`);
|
|
32892
33876
|
mergeIntoDefaultExecutionProfile({
|
|
32893
33877
|
agents,
|
|
32894
33878
|
mergedAllow,
|
|
@@ -34820,7 +35804,10 @@ const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
34820
35804
|
shell: zod_mini.z.optional(zod_mini.z.string()),
|
|
34821
35805
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
34822
35806
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
34823
|
-
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
35807
|
+
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
35808
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
35809
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
35810
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
34824
35811
|
});
|
|
34825
35812
|
/**
|
|
34826
35813
|
* Builds the Claude Code SKILL.md frontmatter from a rulesync skill, carrying
|
|
@@ -34847,7 +35834,10 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
34847
35834
|
hooks: section.hooks,
|
|
34848
35835
|
"disable-model-invocation": resolvedDisableModelInvocation,
|
|
34849
35836
|
"user-invocable": resolvedUserInvocable,
|
|
34850
|
-
paths: section.paths
|
|
35837
|
+
paths: section.paths,
|
|
35838
|
+
license: section.license,
|
|
35839
|
+
compatibility: section.compatibility,
|
|
35840
|
+
metadata: section.metadata
|
|
34851
35841
|
};
|
|
34852
35842
|
const frontmatter = {
|
|
34853
35843
|
name: rulesyncFrontmatter.name,
|
|
@@ -34858,6 +35848,115 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
34858
35848
|
return frontmatter;
|
|
34859
35849
|
}
|
|
34860
35850
|
/**
|
|
35851
|
+
* Builds the `claudecode:` section of a rulesync skill from a Claude Code
|
|
35852
|
+
* SKILL.md frontmatter — the inverse of `buildClaudecodeSkillFrontmatter`, and
|
|
35853
|
+
* extracted for the same reason: to keep `toRulesyncSkill` under the
|
|
35854
|
+
* cyclomatic-complexity cap as fields are added. The truthy/defined split
|
|
35855
|
+
* mirrors that function exactly so the conversion stays symmetric.
|
|
35856
|
+
*/
|
|
35857
|
+
function buildClaudecodeSkillSection({ frontmatter, resolvedPaths, scheduledTask }) {
|
|
35858
|
+
const fields = [
|
|
35859
|
+
[
|
|
35860
|
+
"when_to_use",
|
|
35861
|
+
frontmatter.when_to_use,
|
|
35862
|
+
"truthy"
|
|
35863
|
+
],
|
|
35864
|
+
[
|
|
35865
|
+
"allowed-tools",
|
|
35866
|
+
frontmatter["allowed-tools"],
|
|
35867
|
+
"truthy"
|
|
35868
|
+
],
|
|
35869
|
+
[
|
|
35870
|
+
"disallowed-tools",
|
|
35871
|
+
frontmatter["disallowed-tools"],
|
|
35872
|
+
"truthy"
|
|
35873
|
+
],
|
|
35874
|
+
[
|
|
35875
|
+
"model",
|
|
35876
|
+
frontmatter.model,
|
|
35877
|
+
"truthy"
|
|
35878
|
+
],
|
|
35879
|
+
[
|
|
35880
|
+
"effort",
|
|
35881
|
+
frontmatter.effort,
|
|
35882
|
+
"truthy"
|
|
35883
|
+
],
|
|
35884
|
+
[
|
|
35885
|
+
"argument-hint",
|
|
35886
|
+
frontmatter["argument-hint"],
|
|
35887
|
+
"truthy"
|
|
35888
|
+
],
|
|
35889
|
+
[
|
|
35890
|
+
"arguments",
|
|
35891
|
+
frontmatter.arguments,
|
|
35892
|
+
"defined"
|
|
35893
|
+
],
|
|
35894
|
+
[
|
|
35895
|
+
"context",
|
|
35896
|
+
frontmatter.context,
|
|
35897
|
+
"truthy"
|
|
35898
|
+
],
|
|
35899
|
+
[
|
|
35900
|
+
"agent",
|
|
35901
|
+
frontmatter.agent,
|
|
35902
|
+
"truthy"
|
|
35903
|
+
],
|
|
35904
|
+
[
|
|
35905
|
+
"background",
|
|
35906
|
+
frontmatter.background,
|
|
35907
|
+
"defined"
|
|
35908
|
+
],
|
|
35909
|
+
[
|
|
35910
|
+
"hooks",
|
|
35911
|
+
frontmatter.hooks,
|
|
35912
|
+
"defined"
|
|
35913
|
+
],
|
|
35914
|
+
[
|
|
35915
|
+
"shell",
|
|
35916
|
+
frontmatter.shell,
|
|
35917
|
+
"truthy"
|
|
35918
|
+
],
|
|
35919
|
+
[
|
|
35920
|
+
"disable-model-invocation",
|
|
35921
|
+
frontmatter["disable-model-invocation"],
|
|
35922
|
+
"defined"
|
|
35923
|
+
],
|
|
35924
|
+
[
|
|
35925
|
+
"user-invocable",
|
|
35926
|
+
frontmatter["user-invocable"],
|
|
35927
|
+
"defined"
|
|
35928
|
+
],
|
|
35929
|
+
[
|
|
35930
|
+
"scheduled-task",
|
|
35931
|
+
scheduledTask || void 0,
|
|
35932
|
+
"defined"
|
|
35933
|
+
],
|
|
35934
|
+
[
|
|
35935
|
+
"paths",
|
|
35936
|
+
resolvedPaths,
|
|
35937
|
+
"defined"
|
|
35938
|
+
],
|
|
35939
|
+
[
|
|
35940
|
+
"license",
|
|
35941
|
+
frontmatter.license,
|
|
35942
|
+
"defined"
|
|
35943
|
+
],
|
|
35944
|
+
[
|
|
35945
|
+
"compatibility",
|
|
35946
|
+
frontmatter.compatibility,
|
|
35947
|
+
"defined"
|
|
35948
|
+
],
|
|
35949
|
+
[
|
|
35950
|
+
"metadata",
|
|
35951
|
+
frontmatter.metadata,
|
|
35952
|
+
"defined"
|
|
35953
|
+
]
|
|
35954
|
+
];
|
|
35955
|
+
const section = {};
|
|
35956
|
+
for (const [key, value, presence] of fields) if (presence === "truthy" ? Boolean(value) : value !== void 0) section[key] = value;
|
|
35957
|
+
return section;
|
|
35958
|
+
}
|
|
35959
|
+
/**
|
|
34861
35960
|
* Escapes the glob metacharacters in a directory path so it matches literally.
|
|
34862
35961
|
* A real directory name may contain them — `app/[slug]` in a Next.js tree is
|
|
34863
35962
|
* the common case, and unescaped `[slug]` reads as a bracket expression that
|
|
@@ -34978,25 +36077,11 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34978
36077
|
}
|
|
34979
36078
|
toRulesyncSkill() {
|
|
34980
36079
|
const frontmatter = this.getFrontmatter();
|
|
34981
|
-
const
|
|
34982
|
-
|
|
34983
|
-
|
|
34984
|
-
|
|
34985
|
-
|
|
34986
|
-
...frontmatter.model && { model: frontmatter.model },
|
|
34987
|
-
...frontmatter.effort && { effort: frontmatter.effort },
|
|
34988
|
-
...frontmatter["argument-hint"] && { "argument-hint": frontmatter["argument-hint"] },
|
|
34989
|
-
...frontmatter.arguments !== void 0 && { arguments: frontmatter.arguments },
|
|
34990
|
-
...frontmatter.context && { context: frontmatter.context },
|
|
34991
|
-
...frontmatter.agent && { agent: frontmatter.agent },
|
|
34992
|
-
...frontmatter.background !== void 0 && { background: frontmatter.background },
|
|
34993
|
-
...frontmatter.hooks !== void 0 && { hooks: frontmatter.hooks },
|
|
34994
|
-
...frontmatter.shell && { shell: frontmatter.shell },
|
|
34995
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34996
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34997
|
-
...this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true },
|
|
34998
|
-
...resolvedPaths !== void 0 && { paths: resolvedPaths }
|
|
34999
|
-
};
|
|
36080
|
+
const claudecodeSection = buildClaudecodeSkillSection({
|
|
36081
|
+
frontmatter,
|
|
36082
|
+
resolvedPaths: frontmatter.paths !== void 0 ? frontmatter.paths : deriveNestedSkillPaths(this.relativeDirPath),
|
|
36083
|
+
scheduledTask: this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH
|
|
36084
|
+
});
|
|
35000
36085
|
const rulesyncFrontmatter = {
|
|
35001
36086
|
name: frontmatter.name,
|
|
35002
36087
|
description: frontmatter.description,
|
|
@@ -47857,19 +48942,21 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
47857
48942
|
*/
|
|
47858
48943
|
var PiRule = class PiRule extends ToolRule {
|
|
47859
48944
|
appendSystemPrompt;
|
|
47860
|
-
|
|
48945
|
+
contextFileOverride;
|
|
48946
|
+
constructor({ fileContent, root, appendSystemPrompt = false, contextFileOverride = false, ...rest }) {
|
|
47861
48947
|
super({
|
|
47862
48948
|
...rest,
|
|
47863
48949
|
fileContent,
|
|
47864
48950
|
root: root ?? false
|
|
47865
48951
|
});
|
|
47866
48952
|
this.appendSystemPrompt = appendSystemPrompt;
|
|
48953
|
+
this.contextFileOverride = contextFileOverride;
|
|
47867
48954
|
}
|
|
47868
|
-
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
48955
|
+
static getSettablePaths({ global = false, excludeToolDir, contextFile } = {}) {
|
|
47869
48956
|
return {
|
|
47870
48957
|
root: {
|
|
47871
48958
|
relativeDirPath: global ? buildToolPath(".pi", "agent", excludeToolDir) : ".",
|
|
47872
|
-
relativeFilePath: PI_RULE_FILE_NAME
|
|
48959
|
+
relativeFilePath: contextFile === "override" ? PI_RULE_OVERRIDE_FILE_NAME : PI_RULE_FILE_NAME
|
|
47873
48960
|
},
|
|
47874
48961
|
appendSystemPrompt: {
|
|
47875
48962
|
relativeDirPath: global ? buildToolPath(".pi", "agent", excludeToolDir) : ".pi",
|
|
@@ -47883,7 +48970,22 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47883
48970
|
* `APPEND_SYSTEM.md` is cleaned up once no rule opts in anymore.
|
|
47884
48971
|
*/
|
|
47885
48972
|
static getExtraFixedFiles({ global = false } = {}) {
|
|
47886
|
-
return [this.getSettablePaths({ global }).appendSystemPrompt
|
|
48973
|
+
return [this.getSettablePaths({ global }).appendSystemPrompt, this.getSettablePaths({
|
|
48974
|
+
global,
|
|
48975
|
+
contextFile: "override"
|
|
48976
|
+
}).root];
|
|
48977
|
+
}
|
|
48978
|
+
/**
|
|
48979
|
+
* The project-root `AGENTS.md` is written by several other targets
|
|
48980
|
+
* (agentsmd, codexcli, warp, devin, ...), and the root-file ownership map that
|
|
48981
|
+
* arbitrates a shared path only applies to `--check`. With
|
|
48982
|
+
* `pi.contextFile: override` Pi stops writing that file, so leaving it on the
|
|
48983
|
+
* orphan list would make every `pi` generate delete another target's freshly
|
|
48984
|
+
* written output. The global `~/.pi/agent/AGENTS.md` is Pi-exclusive and stays
|
|
48985
|
+
* deletable.
|
|
48986
|
+
*/
|
|
48987
|
+
isDeletable() {
|
|
48988
|
+
return !(this.getRelativeDirPath() === "." && this.getRelativeFilePath() === "AGENTS.md");
|
|
47887
48989
|
}
|
|
47888
48990
|
/**
|
|
47889
48991
|
* Pi appends `APPEND_SYSTEM.md` to the system prompt itself, so listing it in
|
|
@@ -47908,20 +49010,29 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47908
49010
|
appendSystemPrompt: true
|
|
47909
49011
|
});
|
|
47910
49012
|
}
|
|
47911
|
-
const
|
|
49013
|
+
const isOverride = relativeFilePath === PI_RULE_OVERRIDE_FILE_NAME;
|
|
49014
|
+
const rootPaths = isOverride ? this.getSettablePaths({
|
|
49015
|
+
global,
|
|
49016
|
+
contextFile: "override"
|
|
49017
|
+
}).root : root;
|
|
49018
|
+
const relativePath = (0, node_path.join)(rootPaths.relativeDirPath, rootPaths.relativeFilePath);
|
|
47912
49019
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
47913
49020
|
return new PiRule({
|
|
47914
49021
|
outputRoot,
|
|
47915
|
-
relativeDirPath:
|
|
47916
|
-
relativeFilePath:
|
|
49022
|
+
relativeDirPath: rootPaths.relativeDirPath,
|
|
49023
|
+
relativeFilePath: rootPaths.relativeFilePath,
|
|
47917
49024
|
fileContent,
|
|
47918
49025
|
validate,
|
|
47919
|
-
root: true
|
|
49026
|
+
root: true,
|
|
49027
|
+
contextFileOverride: isOverride
|
|
47920
49028
|
});
|
|
47921
49029
|
}
|
|
47922
49030
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
47923
|
-
const { root, appendSystemPrompt } = this.getSettablePaths({ global });
|
|
47924
49031
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
49032
|
+
const { root, appendSystemPrompt } = this.getSettablePaths({
|
|
49033
|
+
global,
|
|
49034
|
+
contextFile: frontmatter.pi?.contextFile
|
|
49035
|
+
});
|
|
47925
49036
|
if (!frontmatter.root && frontmatter.pi?.systemPrompt === "append") return new PiRule({
|
|
47926
49037
|
outputRoot,
|
|
47927
49038
|
relativeDirPath: appendSystemPrompt.relativeDirPath,
|
|
@@ -47938,7 +49049,8 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47938
49049
|
relativeFilePath: root.relativeFilePath,
|
|
47939
49050
|
fileContent: rulesyncRule.getBody(),
|
|
47940
49051
|
validate,
|
|
47941
|
-
root: isRoot
|
|
49052
|
+
root: isRoot,
|
|
49053
|
+
contextFileOverride: frontmatter.pi?.contextFile === "override"
|
|
47942
49054
|
});
|
|
47943
49055
|
}
|
|
47944
49056
|
toRulesyncRule() {
|
|
@@ -47953,6 +49065,18 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47953
49065
|
},
|
|
47954
49066
|
body: this.getFileContent()
|
|
47955
49067
|
});
|
|
49068
|
+
if (this.contextFileOverride) return new RulesyncRule({
|
|
49069
|
+
outputRoot: process.cwd(),
|
|
49070
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
49071
|
+
relativeFilePath: RULESYNC_OVERVIEW_FILE_NAME,
|
|
49072
|
+
frontmatter: {
|
|
49073
|
+
root: true,
|
|
49074
|
+
targets: ["pi"],
|
|
49075
|
+
globs: ["**/*"],
|
|
49076
|
+
pi: { contextFile: "override" }
|
|
49077
|
+
},
|
|
49078
|
+
body: this.getFileContent()
|
|
49079
|
+
});
|
|
47956
49080
|
return this.toRulesyncRuleDefault();
|
|
47957
49081
|
}
|
|
47958
49082
|
validate() {
|
|
@@ -47972,14 +49096,16 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47972
49096
|
root: false,
|
|
47973
49097
|
appendSystemPrompt: true
|
|
47974
49098
|
});
|
|
47975
|
-
const
|
|
49099
|
+
const isOverride = relativeFilePath === PI_RULE_OVERRIDE_FILE_NAME;
|
|
49100
|
+
const isRoot = (relativeFilePath === "AGENTS.md" || isOverride) && (relativeDirPath === "." || relativeDirPath === root.relativeDirPath);
|
|
47976
49101
|
return new PiRule({
|
|
47977
49102
|
outputRoot,
|
|
47978
49103
|
relativeDirPath,
|
|
47979
49104
|
relativeFilePath,
|
|
47980
49105
|
fileContent: "",
|
|
47981
49106
|
validate: false,
|
|
47982
|
-
root: isRoot
|
|
49107
|
+
root: isRoot,
|
|
49108
|
+
contextFileOverride: isOverride
|
|
47983
49109
|
});
|
|
47984
49110
|
}
|
|
47985
49111
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
@@ -49452,8 +50578,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
49452
50578
|
}
|
|
49453
50579
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
49454
50580
|
const rulesyncRules = rulesyncFiles.filter((file) => file instanceof RulesyncRule);
|
|
49455
|
-
const
|
|
49456
|
-
const
|
|
50581
|
+
const alignedRules = this.alignPiContextFile(rulesyncRules);
|
|
50582
|
+
const localRootRules = alignedRules.filter((rule) => rule.getFrontmatter().localRoot);
|
|
50583
|
+
const nonLocalRootRules = alignedRules.filter((rule) => !rule.getFrontmatter().localRoot);
|
|
49457
50584
|
const factory = this.getFactory(this.toolTarget);
|
|
49458
50585
|
const { meta } = factory;
|
|
49459
50586
|
const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
|
|
@@ -49846,6 +50973,44 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
49846
50973
|
return [...targetedRootRules, ...nonRootRules];
|
|
49847
50974
|
}
|
|
49848
50975
|
/**
|
|
50976
|
+
* Pi reads `AGENTS.override.md` *instead of* `AGENTS.md` from a directory, and
|
|
50977
|
+
* non-root Pi rules are folded into whichever file the root emits. A mix of
|
|
50978
|
+
* opted-in and opted-out rules would therefore split the output across both
|
|
50979
|
+
* files and let Pi silently ignore everything in `AGENTS.md`, so the root rule
|
|
50980
|
+
* decides for all of them: its `pi.contextFile` is copied onto the non-root
|
|
50981
|
+
* rules, and the flag set only on a non-root rule is dropped with a warning.
|
|
50982
|
+
*/
|
|
50983
|
+
alignPiContextFile(rules) {
|
|
50984
|
+
if (this.toolTarget !== "pi") return rules;
|
|
50985
|
+
const factory = this.getFactory(this.toolTarget);
|
|
50986
|
+
const targeted = rules.filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
50987
|
+
const rootContextFile = targeted.some((rule) => rule.getFrontmatter().root === true && rule.getFrontmatter().pi?.contextFile === "override") ? "override" : void 0;
|
|
50988
|
+
const mismatched = targeted.filter((rule) => rule.getFrontmatter().pi?.contextFile !== rootContextFile);
|
|
50989
|
+
if (mismatched.length === 0) return rules;
|
|
50990
|
+
if (rootContextFile === void 0) this.logger.warn(`pi.contextFile is set on ${mismatched.length} non-root rule(s) but not on the root rule, so it is ignored: Pi folds every rule body into the root context file, and emitting AGENTS.override.md for some of them would hide the rest. Set it on the root rule instead: ${formatRulePaths(mismatched)}`);
|
|
50991
|
+
const mismatchedSet = new Set(mismatched);
|
|
50992
|
+
return rules.map((rule) => {
|
|
50993
|
+
if (!mismatchedSet.has(rule)) return rule;
|
|
50994
|
+
const frontmatter = rule.getFrontmatter();
|
|
50995
|
+
const { contextFile: _dropped, ...pi } = frontmatter.pi ?? {};
|
|
50996
|
+
const nextPi = {
|
|
50997
|
+
...pi,
|
|
50998
|
+
...rootContextFile ? { contextFile: rootContextFile } : {}
|
|
50999
|
+
};
|
|
51000
|
+
return new RulesyncRule({
|
|
51001
|
+
outputRoot: rule.getOutputRoot(),
|
|
51002
|
+
relativeDirPath: rule.getRelativeDirPath(),
|
|
51003
|
+
relativeFilePath: rule.getRelativeFilePath(),
|
|
51004
|
+
frontmatter: {
|
|
51005
|
+
...frontmatter,
|
|
51006
|
+
...Object.keys(nextPi).length > 0 ? { pi: nextPi } : { pi: void 0 }
|
|
51007
|
+
},
|
|
51008
|
+
body: rule.getBody(),
|
|
51009
|
+
validate: false
|
|
51010
|
+
});
|
|
51011
|
+
});
|
|
51012
|
+
}
|
|
51013
|
+
/**
|
|
49849
51014
|
* Implementation of abstract method from FeatureProcessor
|
|
49850
51015
|
* Load tool-specific rule configurations and parse them into ToolRule instances
|
|
49851
51016
|
*/
|