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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
2
|
import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
|
|
3
|
-
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
5
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
6
6
|
import os from "node:os";
|
|
@@ -357,6 +357,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
357
357
|
];
|
|
358
358
|
const hooksProcessorToolTargetTuple = [
|
|
359
359
|
"amp",
|
|
360
|
+
"cline",
|
|
360
361
|
"antigravity-cli",
|
|
361
362
|
"antigravity-ide",
|
|
362
363
|
"antigravity-plugin",
|
|
@@ -632,6 +633,29 @@ async function writeFileContent(filepath, content) {
|
|
|
632
633
|
await ensureDir(dirname(filepath));
|
|
633
634
|
await writeFile(filepath, content, "utf-8");
|
|
634
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Apply a POSIX mode to an existing file. Windows has no executable bit and
|
|
638
|
+
* `chmod` there only toggles the read-only flag, so the call is skipped rather
|
|
639
|
+
* than writing a mode the platform cannot honor.
|
|
640
|
+
*/
|
|
641
|
+
async function applyFileMode(filepath, mode) {
|
|
642
|
+
if (process.platform === "win32") return;
|
|
643
|
+
await chmod(filepath, mode);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Restore an executable bit that went missing (interrupted run, a copy that
|
|
647
|
+
* dropped the mode). A file whose mode is merely stricter than `mode` — the
|
|
648
|
+
* user chose 0700 over 0755 — is left alone.
|
|
649
|
+
*/
|
|
650
|
+
async function restoreMissingExecutableBit(filepath, mode) {
|
|
651
|
+
if (process.platform === "win32") return;
|
|
652
|
+
try {
|
|
653
|
+
if (((await stat(filepath)).mode & 73) !== 0) return;
|
|
654
|
+
} catch {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
await chmod(filepath, mode);
|
|
658
|
+
}
|
|
635
659
|
async function writeFileBuffer(filepath, buffer) {
|
|
636
660
|
await ensureDir(dirname(filepath));
|
|
637
661
|
await writeFile(filepath, buffer);
|
|
@@ -1801,6 +1825,13 @@ var AiFile = class {
|
|
|
1801
1825
|
return false;
|
|
1802
1826
|
}
|
|
1803
1827
|
/**
|
|
1828
|
+
* POSIX mode to apply after writing, or `undefined` to leave the default
|
|
1829
|
+
* alone. Override in subclasses whose output the tool executes directly
|
|
1830
|
+
* (e.g. Cline's hook scripts, which are spawned by path and therefore need
|
|
1831
|
+
* the executable bit).
|
|
1832
|
+
*/
|
|
1833
|
+
getFileMode() {}
|
|
1834
|
+
/**
|
|
1804
1835
|
* Returns whether this file can be deleted by rulesync.
|
|
1805
1836
|
* Override in subclasses that should not be deleted (e.g., user-managed config files).
|
|
1806
1837
|
*/
|
|
@@ -2244,7 +2275,8 @@ const HOOK_EVENTS = [
|
|
|
2244
2275
|
"fileChanged",
|
|
2245
2276
|
"directoryAdded",
|
|
2246
2277
|
"elicitation",
|
|
2247
|
-
"elicitationResult"
|
|
2278
|
+
"elicitationResult",
|
|
2279
|
+
"sessionDelete"
|
|
2248
2280
|
];
|
|
2249
2281
|
/** Hook events supported by Cursor. */
|
|
2250
2282
|
const CURSOR_HOOK_EVENTS = [
|
|
@@ -2340,9 +2372,10 @@ const DEVIN_HOOK_EVENTS = [
|
|
|
2340
2372
|
/**
|
|
2341
2373
|
* Hook events supported by OpenCode.
|
|
2342
2374
|
*
|
|
2343
|
-
* `preCompact` maps to `experimental.session.compacting
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
2375
|
+
* `preCompact` maps to `experimental.session.compacting` and
|
|
2376
|
+
* `beforeSubmitPrompt` to `chat.message`, both of which the plugin docs
|
|
2377
|
+
* document as named `(input, output)` hooks rather than `event.type`
|
|
2378
|
+
* dispatches; the other entries are all generic events.
|
|
2346
2379
|
*
|
|
2347
2380
|
* @see https://opencode.ai/docs/plugins/
|
|
2348
2381
|
*/
|
|
@@ -2358,16 +2391,22 @@ const OPENCODE_HOOK_EVENTS = [
|
|
|
2358
2391
|
"preCompact",
|
|
2359
2392
|
"postCompact",
|
|
2360
2393
|
"afterError",
|
|
2361
|
-
"fileChanged"
|
|
2394
|
+
"fileChanged",
|
|
2395
|
+
"notification",
|
|
2396
|
+
"permissionDenied",
|
|
2397
|
+
"beforeSubmitPrompt"
|
|
2362
2398
|
];
|
|
2363
2399
|
/**
|
|
2364
|
-
* Hook events supported by Kilo.
|
|
2365
|
-
*
|
|
2366
|
-
* `file.watcher.updated` and the
|
|
2400
|
+
* Hook events supported by Kilo. Kilo's plugin docs list the same event surface
|
|
2401
|
+
* as OpenCode's — including `session.compacted`, `session.error`,
|
|
2402
|
+
* `file.watcher.updated`, `permission.replied`, `chat.message` and the
|
|
2403
|
+
* experimental compaction hook — with one exception: they document no TUI
|
|
2404
|
+
* events at all, so `tui.toast.show` (canonical `notification`) is left out
|
|
2405
|
+
* rather than emitted into a plugin where it may never fire.
|
|
2367
2406
|
*
|
|
2368
2407
|
* @see https://kilo.ai/docs/automate/extending/plugins
|
|
2369
2408
|
*/
|
|
2370
|
-
const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS;
|
|
2409
|
+
const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS.filter((event) => event !== "notification");
|
|
2371
2410
|
/**
|
|
2372
2411
|
* Hook events supported by Pi Coding Agent, bridged through a generated
|
|
2373
2412
|
* TypeScript extension (Pi has no static hook config file; its extension API
|
|
@@ -2405,6 +2444,29 @@ const AMP_HOOK_EVENTS = [
|
|
|
2405
2444
|
"stop"
|
|
2406
2445
|
];
|
|
2407
2446
|
/**
|
|
2447
|
+
* Hook events supported by Cline's file-based hooks. Cline resolves one
|
|
2448
|
+
* executable per lifecycle event from its hooks directory, and the event names
|
|
2449
|
+
* it accepts are fixed by `VALID_HOOK_TYPES` in
|
|
2450
|
+
* `apps/vscode/src/core/hooks/utils.ts`: `TaskStart`, `TaskResume`,
|
|
2451
|
+
* `TaskCancel`, `TaskComplete`, `PreToolUse`, `PostToolUse`,
|
|
2452
|
+
* `UserPromptSubmit`, `Notification` and `PreCompact`.
|
|
2453
|
+
*
|
|
2454
|
+
* `TaskResume` and `TaskCancel` have no canonical counterpart and stay
|
|
2455
|
+
* unmapped rather than being approximated by `sessionEnd` / `stop`, whose
|
|
2456
|
+
* semantics differ.
|
|
2457
|
+
*
|
|
2458
|
+
* @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
|
|
2459
|
+
*/
|
|
2460
|
+
const CLINE_HOOK_EVENTS = [
|
|
2461
|
+
"sessionStart",
|
|
2462
|
+
"preToolUse",
|
|
2463
|
+
"postToolUse",
|
|
2464
|
+
"beforeSubmitPrompt",
|
|
2465
|
+
"preCompact",
|
|
2466
|
+
"notification",
|
|
2467
|
+
"taskCompleted"
|
|
2468
|
+
];
|
|
2469
|
+
/**
|
|
2408
2470
|
* Hook events supported by GitHub Copilot (cloud coding agent).
|
|
2409
2471
|
*
|
|
2410
2472
|
* GitHub now documents an eight-event surface for `.github/hooks/*.json`:
|
|
@@ -2542,7 +2604,11 @@ const GOOSE_HOOK_EVENTS = [
|
|
|
2542
2604
|
"beforeShellExecution",
|
|
2543
2605
|
"afterShellExecution"
|
|
2544
2606
|
];
|
|
2545
|
-
/**
|
|
2607
|
+
/**
|
|
2608
|
+
* Hook events supported by the embedded agent-config hook format, which only
|
|
2609
|
+
* the deprecated `kiro` alias still writes. See {@link KIRO_IDE_HOOK_EVENTS}
|
|
2610
|
+
* for the standalone format both Kiro products read today.
|
|
2611
|
+
*/
|
|
2546
2612
|
const KIRO_HOOK_EVENTS = [
|
|
2547
2613
|
"sessionStart",
|
|
2548
2614
|
"sessionEnd",
|
|
@@ -2552,15 +2618,17 @@ const KIRO_HOOK_EVENTS = [
|
|
|
2552
2618
|
"stop"
|
|
2553
2619
|
];
|
|
2554
2620
|
/**
|
|
2555
|
-
* Hook events supported by
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
2558
|
-
*
|
|
2559
|
-
*
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
2621
|
+
* Hook events supported by Kiro's standalone hooks format
|
|
2622
|
+
* (`.kiro/hooks/*.json` v1), which the Kiro IDE and Kiro CLI 3.0 both read.
|
|
2623
|
+
*
|
|
2624
|
+
* Kiro exposes PascalCase triggers. rulesync maps the canonical lifecycle
|
|
2625
|
+
* events that have a clean 1:1 equivalent: `SessionStart`, `Stop`,
|
|
2626
|
+
* `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
|
|
2627
|
+
* file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
|
|
2628
|
+
* (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
|
|
2629
|
+
* those can still be emitted verbatim via a `kiro-ide` or `kiro-cli` override
|
|
2630
|
+
* block (unknown event keys pass through unchanged). There is no `SessionEnd`
|
|
2631
|
+
* trigger, so the canonical `sessionEnd` has no home here.
|
|
2564
2632
|
* @see https://kiro.dev/docs/hooks/types/
|
|
2565
2633
|
*/
|
|
2566
2634
|
const KIRO_IDE_HOOK_EVENTS = [
|
|
@@ -2682,7 +2750,8 @@ const QWENCODE_HOOK_EVENTS = [
|
|
|
2682
2750
|
"instructionsLoaded",
|
|
2683
2751
|
"todoCreated",
|
|
2684
2752
|
"todoCompleted",
|
|
2685
|
-
"messageDisplay"
|
|
2753
|
+
"messageDisplay",
|
|
2754
|
+
"sessionDelete"
|
|
2686
2755
|
];
|
|
2687
2756
|
/**
|
|
2688
2757
|
* Hook events supported by Reasonix.
|
|
@@ -2745,7 +2814,11 @@ const GROKCLI_HOOK_EVENTS = [
|
|
|
2745
2814
|
/**
|
|
2746
2815
|
* Hook events supported by Kimi Code.
|
|
2747
2816
|
*
|
|
2748
|
-
* Kimi Code also exposes `Interrupt`,
|
|
2817
|
+
* Kimi Code also exposes `PermissionResult`, `Interrupt`, and the four events
|
|
2818
|
+
* added in 0.32.0 (`TurnStarted`, `UserPromptQueued`, `TaskStarted`,
|
|
2819
|
+
* `SessionHeartbeat`), none of which have a canonical rulesync event. They are
|
|
2820
|
+
* listed in `KIMI_CODE_NATIVE_HOOK_EVENTS` so a per-tool `kimi-code` override
|
|
2821
|
+
* can address them by their native name.
|
|
2749
2822
|
*
|
|
2750
2823
|
* @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
|
|
2751
2824
|
*/
|
|
@@ -2781,10 +2854,27 @@ const CANONICAL_TO_KIMI_CODE_EVENT_NAMES = {
|
|
|
2781
2854
|
preCompact: "PreCompact",
|
|
2782
2855
|
postCompact: "PostCompact"
|
|
2783
2856
|
};
|
|
2857
|
+
/**
|
|
2858
|
+
* Every event name Kimi Code accepts in a `[[hooks]]` entry: the ones with a
|
|
2859
|
+
* canonical rulesync counterpart plus the native-only ones, which are reachable
|
|
2860
|
+
* through a per-tool `kimi-code` override that names them directly.
|
|
2861
|
+
*
|
|
2862
|
+
* `TurnStarted`, `UserPromptQueued`, `TaskStarted`, and `SessionHeartbeat` were
|
|
2863
|
+
* added in Kimi Code 0.32.0. They stay native-only: `TaskStarted` fires when a
|
|
2864
|
+
* background task starts and matches on task kind, whereas the canonical
|
|
2865
|
+
* `taskCreated` models Claude Code's blocking, matcher-less `TaskCreated`
|
|
2866
|
+
* (fired while a task is being created), so the two are not interchangeable.
|
|
2867
|
+
*
|
|
2868
|
+
* @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
|
|
2869
|
+
*/
|
|
2784
2870
|
const KIMI_CODE_NATIVE_HOOK_EVENTS = [
|
|
2785
2871
|
...Object.values(CANONICAL_TO_KIMI_CODE_EVENT_NAMES),
|
|
2786
2872
|
"PermissionResult",
|
|
2787
|
-
"Interrupt"
|
|
2873
|
+
"Interrupt",
|
|
2874
|
+
"TurnStarted",
|
|
2875
|
+
"UserPromptQueued",
|
|
2876
|
+
"TaskStarted",
|
|
2877
|
+
"SessionHeartbeat"
|
|
2788
2878
|
];
|
|
2789
2879
|
const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIMI_CODE_EVENT_NAMES).map(([canonical, kimiCode]) => [kimiCode, canonical]));
|
|
2790
2880
|
/**
|
|
@@ -3061,14 +3151,20 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
|
|
|
3061
3151
|
stop: "session.idle",
|
|
3062
3152
|
afterFileEdit: "file.edited",
|
|
3063
3153
|
permissionRequest: "permission.asked",
|
|
3154
|
+
permissionDenied: "permission.replied",
|
|
3155
|
+
notification: "tui.toast.show",
|
|
3064
3156
|
preCompact: "experimental.session.compacting",
|
|
3157
|
+
beforeSubmitPrompt: "chat.message",
|
|
3065
3158
|
postCompact: "session.compacted",
|
|
3066
3159
|
afterError: "session.error",
|
|
3067
3160
|
fileChanged: "file.watcher.updated"
|
|
3068
3161
|
};
|
|
3069
3162
|
/**
|
|
3070
3163
|
* Map canonical camelCase event names to Kilo dot-notation.
|
|
3071
|
-
*
|
|
3164
|
+
*
|
|
3165
|
+
* Shared with OpenCode: the two name the same events. The `notification` entry
|
|
3166
|
+
* is unreachable for Kilo because `KILO_HOOK_EVENTS` omits it, and the
|
|
3167
|
+
* generator emits only supported events.
|
|
3072
3168
|
*/
|
|
3073
3169
|
const CANONICAL_TO_KILO_EVENT_NAMES = CANONICAL_TO_OPENCODE_EVENT_NAMES;
|
|
3074
3170
|
/**
|
|
@@ -3106,6 +3202,16 @@ const CANONICAL_TO_AMP_EVENT_NAMES = {
|
|
|
3106
3202
|
beforeSubmitPrompt: "agent.start",
|
|
3107
3203
|
stop: "agent.end"
|
|
3108
3204
|
};
|
|
3205
|
+
/** Map canonical hook events to Cline's `VALID_HOOK_TYPES` file names. */
|
|
3206
|
+
const CANONICAL_TO_CLINE_EVENT_NAMES = {
|
|
3207
|
+
sessionStart: "TaskStart",
|
|
3208
|
+
preToolUse: "PreToolUse",
|
|
3209
|
+
postToolUse: "PostToolUse",
|
|
3210
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
3211
|
+
preCompact: "PreCompact",
|
|
3212
|
+
notification: "Notification",
|
|
3213
|
+
taskCompleted: "TaskComplete"
|
|
3214
|
+
};
|
|
3109
3215
|
/**
|
|
3110
3216
|
* Map canonical camelCase event names to Copilot camelCase.
|
|
3111
3217
|
*/
|
|
@@ -3342,7 +3448,8 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
|
|
|
3342
3448
|
instructionsLoaded: "InstructionsLoaded",
|
|
3343
3449
|
todoCreated: "TodoCreated",
|
|
3344
3450
|
todoCompleted: "TodoCompleted",
|
|
3345
|
-
messageDisplay: "MessageDisplay"
|
|
3451
|
+
messageDisplay: "MessageDisplay",
|
|
3452
|
+
sessionDelete: "SessionDelete"
|
|
3346
3453
|
};
|
|
3347
3454
|
/**
|
|
3348
3455
|
* Map Qwen Code PascalCase event names to canonical camelCase.
|
|
@@ -4179,14 +4286,20 @@ const VibePermissionsOverrideSchema = z.looseObject({
|
|
|
4179
4286
|
* Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
|
|
4180
4287
|
* autonomy settings with no canonical permission category — `approvalMode`
|
|
4181
4288
|
* (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
|
|
4182
|
-
* (`mode`/`networkAccess`). Fields placed here are merged into the top
|
|
4183
|
-
*
|
|
4184
|
-
*
|
|
4185
|
-
*
|
|
4186
|
-
*
|
|
4187
|
-
*
|
|
4289
|
+
* (`mode`/`networkAccess`). Fields placed here are merged into the top level of
|
|
4290
|
+
* `~/.cursor/cli-config.json` and emitted only for Cursor, while the shared
|
|
4291
|
+
* `permission` block continues to drive the `permissions.allow`/`permissions.deny`
|
|
4292
|
+
* arrays. Kept a `looseObject` so extra config keys can be authored (they are
|
|
4293
|
+
* merged verbatim on generate); `sandbox`'s accepted values are not documented
|
|
4294
|
+
* so it passes through verbatim.
|
|
4295
|
+
*
|
|
4296
|
+
* These are **global-only** settings: Cursor documents that "Only permissions
|
|
4297
|
+
* can be configured at the project level. All other CLI settings must be set
|
|
4298
|
+
* globally", so a project generate skips them with a warning instead of writing
|
|
4299
|
+
* keys `.cursor/cli.json` would ignore. Author them with `--global`.
|
|
4300
|
+
*
|
|
4188
4301
|
* Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
|
|
4189
|
-
* authored here reach
|
|
4302
|
+
* authored here reach the global config on generate but are not re-extracted.
|
|
4190
4303
|
*
|
|
4191
4304
|
* @example
|
|
4192
4305
|
* { "approvalMode": "auto-review" }
|
|
@@ -4204,7 +4317,10 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
4204
4317
|
* Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
|
|
4205
4318
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
4206
4319
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
4207
|
-
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust
|
|
4320
|
+
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`,
|
|
4321
|
+
* `allowedHttpHookUrls`, `allowPrivateNetworkHooks` — the latter is honored by
|
|
4322
|
+
* Qwen Code only in user/system settings, so generate skips it in project scope).
|
|
4323
|
+
* It also
|
|
4208
4324
|
* exposes `permissions.autoMode` (the Auto Mode classifier config:
|
|
4209
4325
|
* `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` — see
|
|
4210
4326
|
* https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
|
|
@@ -4970,7 +5086,10 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
4970
5086
|
name: z.optional(z.string()),
|
|
4971
5087
|
description: z.optional(z.string())
|
|
4972
5088
|
})),
|
|
4973
|
-
pi: z.optional(z.looseObject({
|
|
5089
|
+
pi: z.optional(z.looseObject({
|
|
5090
|
+
systemPrompt: z.optional(z.enum(["append"])),
|
|
5091
|
+
contextFile: z.optional(z.enum(["override"]))
|
|
5092
|
+
})),
|
|
4974
5093
|
takt: z.optional(z.looseObject({
|
|
4975
5094
|
name: z.optional(z.string()),
|
|
4976
5095
|
extends: z.optional(z.string()),
|
|
@@ -5165,7 +5284,10 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5165
5284
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
5166
5285
|
"user-invocable": z.optional(z.boolean()),
|
|
5167
5286
|
"scheduled-task": z.optional(z.boolean()),
|
|
5168
|
-
paths: z.optional(z.union([z.string(), z.array(z.string())]))
|
|
5287
|
+
paths: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
5288
|
+
license: z.optional(z.string()),
|
|
5289
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
5290
|
+
metadata: z.optional(z.looseObject({}))
|
|
5169
5291
|
})),
|
|
5170
5292
|
codexcli: z.optional(z.looseObject({
|
|
5171
5293
|
"short-description": z.optional(z.string()),
|
|
@@ -5777,13 +5899,20 @@ var FeatureProcessor = class {
|
|
|
5777
5899
|
filePath,
|
|
5778
5900
|
content: contentWithNewline
|
|
5779
5901
|
})) continue;
|
|
5902
|
+
const fileMode = aiFile.getFileMode?.();
|
|
5780
5903
|
if (fileContentsEquivalent({
|
|
5781
5904
|
filePath,
|
|
5782
5905
|
expected: contentWithNewline,
|
|
5783
5906
|
existing: existingContent
|
|
5784
|
-
}))
|
|
5907
|
+
})) {
|
|
5908
|
+
if (fileMode !== void 0 && !this.dryRun) await restoreMissingExecutableBit(filePath, fileMode);
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5785
5911
|
if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${filePath}`);
|
|
5786
|
-
else
|
|
5912
|
+
else {
|
|
5913
|
+
await writeFileContent(filePath, contentWithNewline);
|
|
5914
|
+
if (fileMode !== void 0) await applyFileMode(filePath, fileMode);
|
|
5915
|
+
}
|
|
5787
5916
|
changedCount++;
|
|
5788
5917
|
changedPaths.push(aiFile.getRelativePathFromCwd());
|
|
5789
5918
|
}
|
|
@@ -7144,10 +7273,16 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7144
7273
|
".config/goose/config.yaml": {
|
|
7145
7274
|
format: "yaml",
|
|
7146
7275
|
invalidRootPolicy: "error",
|
|
7147
|
-
features: {
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7276
|
+
features: {
|
|
7277
|
+
mcp: {
|
|
7278
|
+
kind: "replace-owned-keys",
|
|
7279
|
+
ownedKeys: ["extensions"]
|
|
7280
|
+
},
|
|
7281
|
+
commands: {
|
|
7282
|
+
kind: "replace-owned-keys",
|
|
7283
|
+
ownedKeys: ["slash_commands"]
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7151
7286
|
},
|
|
7152
7287
|
[CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
|
|
7153
7288
|
format: "toml",
|
|
@@ -7851,6 +7986,22 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
7851
7986
|
}
|
|
7852
7987
|
};
|
|
7853
7988
|
//#endregion
|
|
7989
|
+
//#region src/constants/goose-paths.ts
|
|
7990
|
+
const GOOSE_DIR = ".goose";
|
|
7991
|
+
const GOOSE_GLOBAL_DIR = join(".config", "goose");
|
|
7992
|
+
const GOOSE_RULE_FILE_NAME = ".goosehints";
|
|
7993
|
+
const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
7994
|
+
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
7995
|
+
const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
|
|
7996
|
+
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
7997
|
+
const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
|
|
7998
|
+
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
7999
|
+
const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
|
|
8000
|
+
const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
|
|
8001
|
+
const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
|
|
8002
|
+
const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
|
|
8003
|
+
const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
|
|
8004
|
+
//#endregion
|
|
7854
8005
|
//#region src/utils/tool-home.ts
|
|
7855
8006
|
/**
|
|
7856
8007
|
* Where the rulesync-side source files of a tool with a home override belong.
|
|
@@ -8792,6 +8943,9 @@ const CLINE_MCP_DIR_PATH = join(CLINE_DIR, "data", "settings");
|
|
|
8792
8943
|
const CLINE_MCP_FILE_NAME = "cline_mcp_settings.json";
|
|
8793
8944
|
const CLINE_PERMISSIONS_FILE_NAME = "command-permissions.json";
|
|
8794
8945
|
const CLINE_IGNORE_FILE_NAME = ".clineignore";
|
|
8946
|
+
const CLINE_HOOKS_DIR_PATH = join(CLINERULES_DIR, "hooks");
|
|
8947
|
+
const CLINE_HOOKS_GLOBAL_DIR_PATH = join("Documents", "Cline", "Hooks");
|
|
8948
|
+
const CLINE_HOOKS_MANIFEST_FILE_NAME = "rulesync-hooks.json";
|
|
8795
8949
|
//#endregion
|
|
8796
8950
|
//#region src/features/commands/cline-command.ts
|
|
8797
8951
|
var ClineCommand = class ClineCommand extends ToolCommand {
|
|
@@ -9253,6 +9407,8 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
|
9253
9407
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
9254
9408
|
const DEVIN_IGNORE_FILE_NAME = ".devinignore";
|
|
9255
9409
|
const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
|
|
9410
|
+
const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
|
|
9411
|
+
const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
|
|
9256
9412
|
//#endregion
|
|
9257
9413
|
//#region src/features/commands/command-skill-ownership.ts
|
|
9258
9414
|
/**
|
|
@@ -9501,24 +9657,99 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
9501
9657
|
}
|
|
9502
9658
|
};
|
|
9503
9659
|
//#endregion
|
|
9504
|
-
//#region src/constants/goose-paths.ts
|
|
9505
|
-
const GOOSE_DIR = ".goose";
|
|
9506
|
-
const GOOSE_GLOBAL_DIR = join(".config", "goose");
|
|
9507
|
-
const GOOSE_RULE_FILE_NAME = ".goosehints";
|
|
9508
|
-
const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
9509
|
-
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
9510
|
-
const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
|
|
9511
|
-
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
9512
|
-
const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
|
|
9513
|
-
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
9514
|
-
const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
|
|
9515
|
-
const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
|
|
9516
|
-
const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
|
|
9517
|
-
const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
|
|
9518
|
-
const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
|
|
9519
|
-
//#endregion
|
|
9520
9660
|
//#region src/features/commands/goose-command.ts
|
|
9521
9661
|
const RECIPE_VERSION = "1.0.0";
|
|
9662
|
+
const SLASH_COMMANDS_KEY = "slash_commands";
|
|
9663
|
+
const GOOSE_GLOBAL_RECIPES_POSIX_DIR = toPosixPath(GOOSE_GLOBAL_RECIPES_DIR_PATH);
|
|
9664
|
+
function slashCommandEntry({ outputRoot, relativeFilePath }) {
|
|
9665
|
+
const fileName = basename(toPosixPath(relativeFilePath));
|
|
9666
|
+
return {
|
|
9667
|
+
command: fileName.replace(/\.ya?ml$/, "").toLowerCase(),
|
|
9668
|
+
recipe_path: join(outputRoot, GOOSE_GLOBAL_RECIPES_DIR_PATH, fileName)
|
|
9669
|
+
};
|
|
9670
|
+
}
|
|
9671
|
+
/**
|
|
9672
|
+
* Whether an existing `slash_commands` entry points at a recipe rulesync owns:
|
|
9673
|
+
* a direct child of the global recipes directory. Anything else — a recipe
|
|
9674
|
+
* elsewhere on disk, or a sub-recipe under `recipes/subagents/` — belongs to
|
|
9675
|
+
* the user and is carried over untouched. A path that cannot be resolved into
|
|
9676
|
+
* the managed directory is preserved rather than claimed.
|
|
9677
|
+
*/
|
|
9678
|
+
function isManagedRecipePath(value) {
|
|
9679
|
+
if (typeof value !== "string") return false;
|
|
9680
|
+
const segments = toPosixPath(value).replace(/^~\//, "").split("/");
|
|
9681
|
+
const fileName = segments.pop();
|
|
9682
|
+
if (fileName === void 0 || fileName === "") return false;
|
|
9683
|
+
const dirSegments = GOOSE_GLOBAL_RECIPES_POSIX_DIR.split("/");
|
|
9684
|
+
return segments.length >= dirSegments.length && segments.slice(-dirSegments.length).join("/") === GOOSE_GLOBAL_RECIPES_POSIX_DIR;
|
|
9685
|
+
}
|
|
9686
|
+
/**
|
|
9687
|
+
* Whether a config file carries a registration rulesync owns. Rewriting a
|
|
9688
|
+
* config that holds none of them would reformat the user's file (comments and
|
|
9689
|
+
* all) for nothing, so both writers check this first.
|
|
9690
|
+
*/
|
|
9691
|
+
function hasManagedGooseSlashCommands(fileContent) {
|
|
9692
|
+
const existing = parseSharedConfig({
|
|
9693
|
+
format: "yaml",
|
|
9694
|
+
fileContent
|
|
9695
|
+
})[SLASH_COMMANDS_KEY];
|
|
9696
|
+
return Array.isArray(existing) && existing.some((entry) => isRecord$1(entry) && isManagedRecipePath(entry.recipe_path));
|
|
9697
|
+
}
|
|
9698
|
+
/**
|
|
9699
|
+
* Recompute `slash_commands` from the entries rulesync generates: user entries
|
|
9700
|
+
* pointing outside the managed recipes directory are carried over, entries
|
|
9701
|
+
* inside it are replaced (so a deleted command's registration is retracted),
|
|
9702
|
+
* and the key is dropped entirely when nothing is left.
|
|
9703
|
+
*/
|
|
9704
|
+
function getGooseSlashCommandsConfigContent({ currentContent, entries }) {
|
|
9705
|
+
const config = parseSharedConfig({
|
|
9706
|
+
format: "yaml",
|
|
9707
|
+
fileContent: currentContent
|
|
9708
|
+
});
|
|
9709
|
+
const next = [...(Array.isArray(config[SLASH_COMMANDS_KEY]) ? config[SLASH_COMMANDS_KEY] : []).filter((entry) => !isRecord$1(entry) || !isManagedRecipePath(entry.recipe_path)), ...entries];
|
|
9710
|
+
return applySharedConfigPatch({
|
|
9711
|
+
fileKey: sharedConfigFileKey({
|
|
9712
|
+
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
9713
|
+
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
9714
|
+
}),
|
|
9715
|
+
feature: "commands",
|
|
9716
|
+
existingContent: currentContent,
|
|
9717
|
+
patch: { [SLASH_COMMANDS_KEY]: next.length > 0 ? next : void 0 }
|
|
9718
|
+
});
|
|
9719
|
+
}
|
|
9720
|
+
/**
|
|
9721
|
+
* The Goose user `config.yaml`, carrying the `slash_commands` registrations for
|
|
9722
|
+
* the generated recipes. The file is shared with the user's own settings, so it
|
|
9723
|
+
* is always merged into rather than replaced.
|
|
9724
|
+
*/
|
|
9725
|
+
var GooseCommandConfigFile = class extends ToolFile {
|
|
9726
|
+
entries;
|
|
9727
|
+
constructor(params) {
|
|
9728
|
+
super(params);
|
|
9729
|
+
this.entries = params.entries;
|
|
9730
|
+
}
|
|
9731
|
+
validate() {
|
|
9732
|
+
return {
|
|
9733
|
+
success: true,
|
|
9734
|
+
error: null
|
|
9735
|
+
};
|
|
9736
|
+
}
|
|
9737
|
+
shouldMergeExistingFileContent() {
|
|
9738
|
+
return true;
|
|
9739
|
+
}
|
|
9740
|
+
setFileContent(newFileContent) {
|
|
9741
|
+
super.setFileContent(getGooseSlashCommandsConfigContent({
|
|
9742
|
+
currentContent: newFileContent,
|
|
9743
|
+
entries: this.entries
|
|
9744
|
+
}));
|
|
9745
|
+
}
|
|
9746
|
+
getFileContent() {
|
|
9747
|
+
return getGooseSlashCommandsConfigContent({
|
|
9748
|
+
currentContent: super.getFileContent(),
|
|
9749
|
+
entries: this.entries
|
|
9750
|
+
});
|
|
9751
|
+
}
|
|
9752
|
+
};
|
|
9522
9753
|
/**
|
|
9523
9754
|
* Goose recipe files are reusable YAML workflow documents. A recipe requires
|
|
9524
9755
|
* `version`, `title`, and `description`, plus at least one of `instructions` /
|
|
@@ -9553,6 +9784,40 @@ var GooseCommand = class GooseCommand extends ToolCommand {
|
|
|
9553
9784
|
static getSettablePaths({ global = false } = {}) {
|
|
9554
9785
|
return { relativeDirPath: global ? GOOSE_GLOBAL_RECIPES_DIR_PATH : GOOSE_RECIPES_DIR_PATH };
|
|
9555
9786
|
}
|
|
9787
|
+
/**
|
|
9788
|
+
* The user `config.yaml` holding the `slash_commands` registrations. Global
|
|
9789
|
+
* scope only — Goose has no project-level registration surface.
|
|
9790
|
+
*/
|
|
9791
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
9792
|
+
if (!global) return [];
|
|
9793
|
+
return [{
|
|
9794
|
+
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
9795
|
+
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
9796
|
+
}];
|
|
9797
|
+
}
|
|
9798
|
+
/**
|
|
9799
|
+
* Register the generated recipes as slash commands. The config file is also
|
|
9800
|
+
* emitted when no command is generated but the existing file still carries
|
|
9801
|
+
* managed registrations, so removing the last command retracts them instead of
|
|
9802
|
+
* leaving `/name` pointing at a deleted recipe.
|
|
9803
|
+
*/
|
|
9804
|
+
static async getAuxiliaryFiles({ toolCommands, outputRoot = process.cwd(), global = false, forDeletion = false }) {
|
|
9805
|
+
if (!global || forDeletion) return [];
|
|
9806
|
+
const entries = toolCommands.map((command) => slashCommandEntry({
|
|
9807
|
+
outputRoot,
|
|
9808
|
+
relativeFilePath: command.getRelativeFilePath()
|
|
9809
|
+
}));
|
|
9810
|
+
const existingContent = await readFileContentOrNull(join(outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME));
|
|
9811
|
+
if (entries.length === 0 && !hasManagedGooseSlashCommands(existingContent ?? "")) return [];
|
|
9812
|
+
return [new GooseCommandConfigFile({
|
|
9813
|
+
outputRoot,
|
|
9814
|
+
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
9815
|
+
relativeFilePath: GOOSE_MCP_FILE_NAME,
|
|
9816
|
+
fileContent: existingContent ?? "",
|
|
9817
|
+
entries,
|
|
9818
|
+
global
|
|
9819
|
+
})];
|
|
9820
|
+
}
|
|
9556
9821
|
parseRecipeContent(content) {
|
|
9557
9822
|
const where = join(this.relativeDirPath, this.relativeFilePath);
|
|
9558
9823
|
let parsed;
|
|
@@ -11038,8 +11303,9 @@ const KIRO_SETTINGS_DIR_PATH = join(KIRO_DIR, "settings");
|
|
|
11038
11303
|
const KIRO_AGENTS_DIR_PATH = join(KIRO_DIR, "agents");
|
|
11039
11304
|
const KIRO_HOOKS_FILE_NAME = "default.json";
|
|
11040
11305
|
/**
|
|
11041
|
-
* Kiro
|
|
11042
|
-
*
|
|
11306
|
+
* Kiro stores hooks as structured JSON files in `.kiro/hooks/` (workspace) and
|
|
11307
|
+
* `~/.kiro/hooks/` (user) — the format the IDE reads and the one Kiro CLI 3.0
|
|
11308
|
+
* migrated to. A single file may declare multiple
|
|
11043
11309
|
* hooks in its `hooks` array, so rulesync emits all generated hooks into one
|
|
11044
11310
|
* `rulesync.json` file per scope.
|
|
11045
11311
|
* @see https://kiro.dev/docs/hooks/
|
|
@@ -11382,6 +11648,7 @@ const PI_EXTENSIONS_DIR_PATH = join(".pi", "extensions");
|
|
|
11382
11648
|
const PI_PROMPTS_DIR_PATH = join(".pi", "prompts");
|
|
11383
11649
|
const PI_SKILLS_DIR_PATH = join(".pi", "skills");
|
|
11384
11650
|
const PI_RULE_FILE_NAME = "AGENTS.md";
|
|
11651
|
+
const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
|
|
11385
11652
|
const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
|
|
11386
11653
|
const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
|
|
11387
11654
|
//#endregion
|
|
@@ -12873,6 +13140,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
12873
13140
|
}));
|
|
12874
13141
|
const shouldDisableHermesCommandsPlugin = this.toolTarget === "hermesagent" && existingFiles.some((file) => file.getFilePath() === ownershipPath) && !generatedFiles.some((file) => file.getFilePath() === ownershipPath);
|
|
12875
13142
|
let changedCount = await super.removeOrphanAiFiles(existingFiles, generatedFiles);
|
|
13143
|
+
changedCount += await this.retractGooseSlashCommands(generatedFiles);
|
|
12876
13144
|
if (!shouldDisableHermesCommandsPlugin) return changedCount;
|
|
12877
13145
|
const configPath = join(this.outputRoot, getHermesagentRelativeFilePath({
|
|
12878
13146
|
global: this.global,
|
|
@@ -12891,6 +13159,27 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
12891
13159
|
return changedCount;
|
|
12892
13160
|
}
|
|
12893
13161
|
/**
|
|
13162
|
+
* Drop the `slash_commands` registrations when no Goose recipe is generated
|
|
13163
|
+
* any more. `GooseCommand.getAuxiliaryFiles` handles every other case, but it
|
|
13164
|
+
* is not reached when the whole feature has no source files left (`--delete`
|
|
13165
|
+
* removes the recipes there), which would strand `/name` on a deleted recipe.
|
|
13166
|
+
*/
|
|
13167
|
+
async retractGooseSlashCommands(generatedFiles) {
|
|
13168
|
+
if (this.toolTarget !== "goose" || !this.global) return 0;
|
|
13169
|
+
if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
|
|
13170
|
+
const configPath = join(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
|
|
13171
|
+
const currentContent = await readFileContentOrNull(configPath);
|
|
13172
|
+
if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
|
|
13173
|
+
const nextContent = getGooseSlashCommandsConfigContent({
|
|
13174
|
+
currentContent,
|
|
13175
|
+
entries: []
|
|
13176
|
+
});
|
|
13177
|
+
if (nextContent === currentContent) return 0;
|
|
13178
|
+
if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
|
|
13179
|
+
else await writeFileContent(configPath, nextContent);
|
|
13180
|
+
return 1;
|
|
13181
|
+
}
|
|
13182
|
+
/**
|
|
12894
13183
|
* Implementation of abstract method from FeatureProcessor
|
|
12895
13184
|
* Return the tool targets that this processor supports
|
|
12896
13185
|
*/
|
|
@@ -13059,6 +13348,14 @@ var ToolHooks = class extends ToolFile {
|
|
|
13059
13348
|
static async getAuxiliaryFiles(_params) {
|
|
13060
13349
|
return [];
|
|
13061
13350
|
}
|
|
13351
|
+
/**
|
|
13352
|
+
* Extra files the deletion sweep may remove, for adapters that write more
|
|
13353
|
+
* than their settable path. Kept separate from {@link getAuxiliaryFiles},
|
|
13354
|
+
* which may legitimately return a shared user-owned config file.
|
|
13355
|
+
*/
|
|
13356
|
+
static async getDeletableAuxiliaryFiles(_params) {
|
|
13357
|
+
return [];
|
|
13358
|
+
}
|
|
13062
13359
|
};
|
|
13063
13360
|
//#endregion
|
|
13064
13361
|
//#region src/features/hooks/amp-hooks.ts
|
|
@@ -14341,8 +14638,7 @@ const CLAUDE_CONVERTER_CONFIG = {
|
|
|
14341
14638
|
"teammateIdle",
|
|
14342
14639
|
"cwdChanged",
|
|
14343
14640
|
"beforeSubmitPrompt",
|
|
14344
|
-
"stop"
|
|
14345
|
-
"directoryAdded"
|
|
14641
|
+
"stop"
|
|
14346
14642
|
]),
|
|
14347
14643
|
supportedHookTypes: /* @__PURE__ */ new Set([
|
|
14348
14644
|
"command",
|
|
@@ -14516,6 +14812,281 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
|
|
|
14516
14812
|
}
|
|
14517
14813
|
};
|
|
14518
14814
|
//#endregion
|
|
14815
|
+
//#region src/features/hooks/cline-hooks-generator.ts
|
|
14816
|
+
/**
|
|
14817
|
+
* Marker line every generated hook script carries. Cline resolves hooks by
|
|
14818
|
+
* exact event name from a directory users also hand-author scripts in, so the
|
|
14819
|
+
* marker is what tells a rulesync-owned script apart from a user's own: only
|
|
14820
|
+
* files carrying it are rewritten or cleaned up.
|
|
14821
|
+
*/
|
|
14822
|
+
const CLINE_HOOK_SCRIPT_MARKER = "rulesync-owned: cline-hooks";
|
|
14823
|
+
/** Exit code a hook command uses to cancel the task (Claude Code convention). */
|
|
14824
|
+
const CANCEL_EXIT_CODE = 2;
|
|
14825
|
+
function sanitizeCommand(command) {
|
|
14826
|
+
let sanitized = command;
|
|
14827
|
+
for (const char of CONTROL_CHARS) sanitized = sanitized.replaceAll(char, "");
|
|
14828
|
+
return sanitized;
|
|
14829
|
+
}
|
|
14830
|
+
/** Single-quote a string for POSIX shells. */
|
|
14831
|
+
function shellQuote(value) {
|
|
14832
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
14833
|
+
}
|
|
14834
|
+
/** Single-quote a string for PowerShell. */
|
|
14835
|
+
function powerShellQuote(value) {
|
|
14836
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
14837
|
+
}
|
|
14838
|
+
function collectClineHookCommands({ effectiveHooks, eventMap }) {
|
|
14839
|
+
const commandsByEvent = {};
|
|
14840
|
+
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
14841
|
+
const clineEvent = eventMap[canonicalEvent];
|
|
14842
|
+
if (!clineEvent) continue;
|
|
14843
|
+
const commands = definitions.filter((definition) => (definition.type ?? "command") === "command" && definition.command).map((definition) => sanitizeCommand(definition.command)).filter((command) => command.trim() !== "");
|
|
14844
|
+
if (commands.length === 0) continue;
|
|
14845
|
+
const existing = commandsByEvent[clineEvent];
|
|
14846
|
+
if (existing) existing.push(...commands);
|
|
14847
|
+
else commandsByEvent[clineEvent] = commands;
|
|
14848
|
+
}
|
|
14849
|
+
return commandsByEvent;
|
|
14850
|
+
}
|
|
14851
|
+
/**
|
|
14852
|
+
* A POSIX wrapper script for one Cline hook event.
|
|
14853
|
+
*
|
|
14854
|
+
* Cline spawns the file itself (`spawn(scriptPath, [], { shell: true })`), feeds
|
|
14855
|
+
* the event payload on stdin and reads a JSON result from stdout. The wrapper
|
|
14856
|
+
* therefore forwards the payload to each configured command in order and
|
|
14857
|
+
* translates the exit codes: `2` cancels the task, any other failure is
|
|
14858
|
+
* reported through `errorMessage` without cancelling, mirroring how the
|
|
14859
|
+
* canonical `command` hook type behaves for the tools that support blocking.
|
|
14860
|
+
*/
|
|
14861
|
+
function generateClineHookScript({ event, commands }) {
|
|
14862
|
+
const lines = [
|
|
14863
|
+
"#!/bin/bash",
|
|
14864
|
+
`# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
|
|
14865
|
+
`# ${CLINE_HOOK_SCRIPT_MARKER}`,
|
|
14866
|
+
"",
|
|
14867
|
+
"payload=$(cat)",
|
|
14868
|
+
"cancel=false",
|
|
14869
|
+
"error_message=''",
|
|
14870
|
+
""
|
|
14871
|
+
];
|
|
14872
|
+
for (const command of commands) {
|
|
14873
|
+
const quoted = shellQuote(command);
|
|
14874
|
+
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", "");
|
|
14875
|
+
}
|
|
14876
|
+
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")"`, "");
|
|
14877
|
+
return lines.join("\n");
|
|
14878
|
+
}
|
|
14879
|
+
/**
|
|
14880
|
+
* The PowerShell twin of {@link generateClineHookScript}. On Windows Cline
|
|
14881
|
+
* resolves only `<Event>.ps1` and runs it through `powershell -File`, so both
|
|
14882
|
+
* spellings are written and the platform picks one.
|
|
14883
|
+
*/
|
|
14884
|
+
function generateClineHookPowerShellScript({ event, commands }) {
|
|
14885
|
+
const lines = [
|
|
14886
|
+
`# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
|
|
14887
|
+
`# ${CLINE_HOOK_SCRIPT_MARKER}`,
|
|
14888
|
+
"",
|
|
14889
|
+
"$payload = [Console]::In.ReadToEnd()",
|
|
14890
|
+
"$cancel = $false",
|
|
14891
|
+
"$errorMessage = ''",
|
|
14892
|
+
""
|
|
14893
|
+
];
|
|
14894
|
+
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", " }", "}", "");
|
|
14895
|
+
lines.push("@{", " cancel = $cancel", " contextModification = \"\"", " errorMessage = $errorMessage", "} | ConvertTo-Json -Compress", "");
|
|
14896
|
+
return lines.join("\n");
|
|
14897
|
+
}
|
|
14898
|
+
//#endregion
|
|
14899
|
+
//#region src/features/hooks/cline-hooks.ts
|
|
14900
|
+
/** Mode Cline's hook scripts need: it spawns the file itself on Unix. */
|
|
14901
|
+
const HOOK_SCRIPT_MODE = 493;
|
|
14902
|
+
/** The only file names this adapter ever writes into the hooks directory. */
|
|
14903
|
+
const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES));
|
|
14904
|
+
/**
|
|
14905
|
+
* Read the manifest of a previous run. Event names are filtered against the
|
|
14906
|
+
* names this adapter emits: the manifest is a file in the repository, and
|
|
14907
|
+
* anything else there would otherwise be turned into a path — an executable one
|
|
14908
|
+
* — that rulesync writes.
|
|
14909
|
+
*/
|
|
14910
|
+
function parseManifest(fileContent) {
|
|
14911
|
+
try {
|
|
14912
|
+
const parsed = JSON.parse(fileContent);
|
|
14913
|
+
if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
|
|
14914
|
+
return {
|
|
14915
|
+
generatedBy: "rulesync",
|
|
14916
|
+
events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
|
|
14917
|
+
};
|
|
14918
|
+
} catch {
|
|
14919
|
+
return null;
|
|
14920
|
+
}
|
|
14921
|
+
}
|
|
14922
|
+
/** One generated hook script. Written executable so Cline can spawn it. */
|
|
14923
|
+
var ClineHookScript = class extends ToolFile {
|
|
14924
|
+
getFileMode() {
|
|
14925
|
+
return this.getRelativeFilePath().endsWith(".ps1") ? void 0 : HOOK_SCRIPT_MODE;
|
|
14926
|
+
}
|
|
14927
|
+
validate() {
|
|
14928
|
+
return {
|
|
14929
|
+
success: true,
|
|
14930
|
+
error: null
|
|
14931
|
+
};
|
|
14932
|
+
}
|
|
14933
|
+
};
|
|
14934
|
+
/**
|
|
14935
|
+
* Hooks adapter for Cline's file-based hooks.
|
|
14936
|
+
*
|
|
14937
|
+
* Cline (VS Code extension / Cline Desktop) resolves one executable per
|
|
14938
|
+
* lifecycle event from `<project>/.clinerules/hooks/` or the global
|
|
14939
|
+
* `~/Documents/Cline/Hooks/`, named exactly after the event: the extensionless
|
|
14940
|
+
* name on Unix, `<Event>.ps1` on Windows. The script receives the event payload
|
|
14941
|
+
* as JSON on stdin and answers with `{"cancel": …, "contextModification": …,
|
|
14942
|
+
* "errorMessage": …}` on stdout. rulesync emits a wrapper script per configured
|
|
14943
|
+
* event in both spellings, plus a `rulesync-hooks.json` manifest naming the
|
|
14944
|
+
* scripts it owns.
|
|
14945
|
+
*
|
|
14946
|
+
* The directory is shared with hand-authored hooks and the filenames are fixed
|
|
14947
|
+
* by the contract, so every generated script carries a marker line and a script
|
|
14948
|
+
* without it is never overwritten.
|
|
14949
|
+
*
|
|
14950
|
+
* Cline's CLI and SDK use a different, in-process hook surface (`AgentHooks`
|
|
14951
|
+
* from `@cline/core`), which this adapter does not target.
|
|
14952
|
+
*
|
|
14953
|
+
* @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
|
|
14954
|
+
*/
|
|
14955
|
+
var ClineHooks = class ClineHooks extends ToolHooks {
|
|
14956
|
+
scriptsByEvent;
|
|
14957
|
+
constructor(params) {
|
|
14958
|
+
super({
|
|
14959
|
+
...params,
|
|
14960
|
+
fileContent: params.fileContent ?? ""
|
|
14961
|
+
});
|
|
14962
|
+
this.scriptsByEvent = params.scriptsByEvent ?? {};
|
|
14963
|
+
}
|
|
14964
|
+
static getSettablePaths(options) {
|
|
14965
|
+
return {
|
|
14966
|
+
relativeDirPath: options?.global ? CLINE_HOOKS_GLOBAL_DIR_PATH : CLINE_HOOKS_DIR_PATH,
|
|
14967
|
+
relativeFilePath: CLINE_HOOKS_MANIFEST_FILE_NAME
|
|
14968
|
+
};
|
|
14969
|
+
}
|
|
14970
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14971
|
+
const paths = ClineHooks.getSettablePaths({ global });
|
|
14972
|
+
const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
14973
|
+
return new ClineHooks({
|
|
14974
|
+
outputRoot,
|
|
14975
|
+
...paths,
|
|
14976
|
+
fileContent,
|
|
14977
|
+
validate
|
|
14978
|
+
});
|
|
14979
|
+
}
|
|
14980
|
+
static fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
14981
|
+
const config = rulesyncHooks.getJson();
|
|
14982
|
+
const overrideHooks = config.cline?.hooks ?? {};
|
|
14983
|
+
const scriptsByEvent = collectClineHookCommands({
|
|
14984
|
+
effectiveHooks: {
|
|
14985
|
+
...config.hooks,
|
|
14986
|
+
...overrideHooks
|
|
14987
|
+
},
|
|
14988
|
+
eventMap: CANONICAL_TO_CLINE_EVENT_NAMES
|
|
14989
|
+
});
|
|
14990
|
+
const manifest = {
|
|
14991
|
+
generatedBy: "rulesync",
|
|
14992
|
+
events: Object.keys(scriptsByEvent).toSorted()
|
|
14993
|
+
};
|
|
14994
|
+
return new ClineHooks({
|
|
14995
|
+
outputRoot,
|
|
14996
|
+
...ClineHooks.getSettablePaths({ global }),
|
|
14997
|
+
fileContent: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
14998
|
+
validate,
|
|
14999
|
+
scriptsByEvent
|
|
15000
|
+
});
|
|
15001
|
+
}
|
|
15002
|
+
/**
|
|
15003
|
+
* The per-event scripts, plus a neutralized script for every event a previous
|
|
15004
|
+
* run generated and this one no longer covers — those files stay on disk (the
|
|
15005
|
+
* hooks feature only reconciles its single settable path), so they are
|
|
15006
|
+
* rewritten as no-ops instead of being left running a removed hook.
|
|
15007
|
+
*/
|
|
15008
|
+
async getScriptFiles({ global = false, logger } = {}) {
|
|
15009
|
+
const paths = ClineHooks.getSettablePaths({ global });
|
|
15010
|
+
const previous = parseManifest(await readFileContentOrNull(join(this.outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "")?.events ?? [];
|
|
15011
|
+
const events = [.../* @__PURE__ */ new Set([...Object.keys(this.scriptsByEvent), ...previous])].toSorted();
|
|
15012
|
+
const files = [];
|
|
15013
|
+
for (const event of events) {
|
|
15014
|
+
const commands = this.scriptsByEvent[event] ?? [];
|
|
15015
|
+
for (const [relativeFilePath, fileContent] of [[event, generateClineHookScript({
|
|
15016
|
+
event,
|
|
15017
|
+
commands
|
|
15018
|
+
})], [`${event}.ps1`, generateClineHookPowerShellScript({
|
|
15019
|
+
event,
|
|
15020
|
+
commands
|
|
15021
|
+
})]]) {
|
|
15022
|
+
const existing = await readFileContentOrNull(join(this.outputRoot, paths.relativeDirPath, relativeFilePath));
|
|
15023
|
+
if (existing !== null && !existing.includes("rulesync-owned: cline-hooks")) {
|
|
15024
|
+
logger?.warn(`Kept the existing ${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.`);
|
|
15025
|
+
continue;
|
|
15026
|
+
}
|
|
15027
|
+
files.push(new ClineHookScript({
|
|
15028
|
+
outputRoot: this.outputRoot,
|
|
15029
|
+
relativeDirPath: paths.relativeDirPath,
|
|
15030
|
+
relativeFilePath,
|
|
15031
|
+
fileContent
|
|
15032
|
+
}));
|
|
15033
|
+
}
|
|
15034
|
+
}
|
|
15035
|
+
return files;
|
|
15036
|
+
}
|
|
15037
|
+
/**
|
|
15038
|
+
* The generated scripts ride alongside the manifest. Only a `ClineHooks`
|
|
15039
|
+
* instance can produce them, so the processor hands its freshly built one
|
|
15040
|
+
* back here.
|
|
15041
|
+
*/
|
|
15042
|
+
static async getAuxiliaryFiles({ global = false, toolHooks, logger } = {}) {
|
|
15043
|
+
if (!(toolHooks instanceof ClineHooks)) return [];
|
|
15044
|
+
return toolHooks.getScriptFiles({
|
|
15045
|
+
global,
|
|
15046
|
+
logger
|
|
15047
|
+
});
|
|
15048
|
+
}
|
|
15049
|
+
/**
|
|
15050
|
+
* Every rulesync-marked script currently on disk. Dropping the target must
|
|
15051
|
+
* take the scripts with it: they hold the actual commands, and leaving them
|
|
15052
|
+
* behind keeps a removed hook running.
|
|
15053
|
+
*/
|
|
15054
|
+
static async getDeletableAuxiliaryFiles({ outputRoot = process.cwd(), global = false } = {}) {
|
|
15055
|
+
const paths = ClineHooks.getSettablePaths({ global });
|
|
15056
|
+
const files = [];
|
|
15057
|
+
for (const event of [...MANAGED_EVENT_NAMES].toSorted()) for (const relativeFilePath of [event, `${event}.ps1`]) {
|
|
15058
|
+
const existing = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, relativeFilePath));
|
|
15059
|
+
if (existing === null || !existing.includes("rulesync-owned: cline-hooks")) continue;
|
|
15060
|
+
files.push(new ClineHookScript({
|
|
15061
|
+
outputRoot,
|
|
15062
|
+
relativeDirPath: paths.relativeDirPath,
|
|
15063
|
+
relativeFilePath,
|
|
15064
|
+
fileContent: "",
|
|
15065
|
+
validate: false
|
|
15066
|
+
}));
|
|
15067
|
+
}
|
|
15068
|
+
return files;
|
|
15069
|
+
}
|
|
15070
|
+
toRulesyncHooks() {
|
|
15071
|
+
throw new Error("Not implemented because generated Cline hook scripts cannot be imported back into canonical hooks.");
|
|
15072
|
+
}
|
|
15073
|
+
validate() {
|
|
15074
|
+
return {
|
|
15075
|
+
success: true,
|
|
15076
|
+
error: null
|
|
15077
|
+
};
|
|
15078
|
+
}
|
|
15079
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
15080
|
+
return new ClineHooks({
|
|
15081
|
+
outputRoot,
|
|
15082
|
+
relativeDirPath,
|
|
15083
|
+
relativeFilePath,
|
|
15084
|
+
fileContent: "",
|
|
15085
|
+
validate: false
|
|
15086
|
+
});
|
|
15087
|
+
}
|
|
15088
|
+
};
|
|
15089
|
+
//#endregion
|
|
14519
15090
|
//#region src/features/hooks/codexcli-hooks.ts
|
|
14520
15091
|
const CODEXCLI_CONVERTER_CONFIG = {
|
|
14521
15092
|
supportedEvents: CODEXCLI_HOOK_EVENTS,
|
|
@@ -14682,12 +15253,19 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
14682
15253
|
/**
|
|
14683
15254
|
* Copilot hook entry as stored in .github/hooks/copilot-hooks.json.
|
|
14684
15255
|
*
|
|
14685
|
-
*
|
|
15256
|
+
* The canonical `shell` selector chooses `bash` or `powershell`; without it the
|
|
15257
|
+
* portable `command` field is written, which upstream copies to both when
|
|
15258
|
+
* neither is present. Note the cloud agent runs hooks in a Linux sandbox and
|
|
15259
|
+
* honors only `bash` and `command` — a `powershell` entry is ignored there.
|
|
15260
|
+
*
|
|
15261
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
14686
15262
|
*/
|
|
14687
15263
|
const CopilotHookEntrySchema = z.looseObject({
|
|
14688
15264
|
type: z.string(),
|
|
14689
15265
|
bash: z.optional(z.string()),
|
|
14690
15266
|
powershell: z.optional(z.string()),
|
|
15267
|
+
command: z.optional(z.string()),
|
|
15268
|
+
env: z.optional(z.record(z.string(), z.string())),
|
|
14691
15269
|
timeoutSec: z.optional(z.number())
|
|
14692
15270
|
});
|
|
14693
15271
|
/**
|
|
@@ -14695,12 +15273,16 @@ const CopilotHookEntrySchema = z.looseObject({
|
|
|
14695
15273
|
* Filters shared hooks to COPILOT_HOOK_EVENTS, merges config.copilot?.hooks,
|
|
14696
15274
|
* then converts to Copilot event names and field format.
|
|
14697
15275
|
*
|
|
14698
|
-
*
|
|
14699
|
-
*
|
|
15276
|
+
* The command field is chosen by the canonical `shell` selector, falling back to
|
|
15277
|
+
* the portable `command` field — never by the platform Rulesync happens to run
|
|
15278
|
+
* on. Keying it off `process.platform` meant a file generated on Windows carried
|
|
15279
|
+
* only `powershell`, which the Linux-sandboxed cloud agent ignores outright, so
|
|
15280
|
+
* the hook silently never ran; it also made the output differ per generating
|
|
15281
|
+
* machine, which shows up as churn for anyone who checks the file in (the cloud
|
|
15282
|
+
* agent reads it from the repository).
|
|
14700
15283
|
*/
|
|
14701
15284
|
function canonicalToCopilotHooks(config) {
|
|
14702
15285
|
const canonicalSchemaKeys = Object.keys(HookDefinitionSchema.shape);
|
|
14703
|
-
const commandField = process.platform === "win32" ? "powershell" : "bash";
|
|
14704
15286
|
const supported = new Set(COPILOT_HOOK_EVENTS);
|
|
14705
15287
|
const sharedConfigHooks = {};
|
|
14706
15288
|
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedConfigHooks[event] = defs;
|
|
@@ -14718,10 +15300,12 @@ function canonicalToCopilotHooks(config) {
|
|
|
14718
15300
|
if (hookType !== "command") continue;
|
|
14719
15301
|
const command = def.command;
|
|
14720
15302
|
const timeout = def.timeout;
|
|
15303
|
+
const commandField = def.shell ?? "command";
|
|
14721
15304
|
const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
|
|
14722
15305
|
entries.push({
|
|
14723
15306
|
type: hookType,
|
|
14724
15307
|
...command !== void 0 && command !== null && { [commandField]: command },
|
|
15308
|
+
...def.env !== void 0 && { env: def.env },
|
|
14725
15309
|
...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
|
|
14726
15310
|
...rest
|
|
14727
15311
|
});
|
|
@@ -14731,24 +15315,35 @@ function canonicalToCopilotHooks(config) {
|
|
|
14731
15315
|
return copilot;
|
|
14732
15316
|
}
|
|
14733
15317
|
/**
|
|
14734
|
-
* Resolve the command
|
|
15318
|
+
* Resolve the command and its shell selector from a Copilot hook entry.
|
|
14735
15319
|
*
|
|
14736
|
-
* - If only
|
|
14737
|
-
* -
|
|
14738
|
-
* - If both are present,
|
|
14739
|
-
*
|
|
15320
|
+
* - If only one shell-specific field is present, use it and record the shell,
|
|
15321
|
+
* so a re-export writes the same field back.
|
|
15322
|
+
* - If both are present, take `bash` and warn. The choice is deliberately not
|
|
15323
|
+
* platform-dependent: the cloud agent runs hooks in a Linux sandbox and
|
|
15324
|
+
* ignores `powershell` entirely, and importing on Windows must not produce a
|
|
15325
|
+
* different canonical config than importing the same file on Linux.
|
|
15326
|
+
* - Otherwise fall back to the portable `command` field, leaving `shell` unset
|
|
15327
|
+
* so a re-export renders the portable field again.
|
|
14740
15328
|
*/
|
|
14741
15329
|
function resolveImportCommand$1(entry, logger) {
|
|
14742
15330
|
const hasBash = typeof entry.bash === "string";
|
|
14743
15331
|
const hasPowershell = typeof entry.powershell === "string";
|
|
14744
15332
|
if (hasBash && hasPowershell) {
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
} else if (hasBash) return
|
|
14751
|
-
|
|
15333
|
+
logger?.warn("Copilot hook has both bash and powershell commands; using bash and ignoring powershell, which the Linux-sandboxed cloud agent does not run.");
|
|
15334
|
+
return {
|
|
15335
|
+
command: entry.bash,
|
|
15336
|
+
shell: "bash"
|
|
15337
|
+
};
|
|
15338
|
+
} else if (hasBash) return {
|
|
15339
|
+
command: entry.bash,
|
|
15340
|
+
shell: "bash"
|
|
15341
|
+
};
|
|
15342
|
+
else if (hasPowershell) return {
|
|
15343
|
+
command: entry.powershell,
|
|
15344
|
+
shell: "powershell"
|
|
15345
|
+
};
|
|
15346
|
+
return typeof entry.command === "string" ? { command: entry.command } : {};
|
|
14752
15347
|
}
|
|
14753
15348
|
/**
|
|
14754
15349
|
* Extract hooks from Copilot hooks JSON into canonical format.
|
|
@@ -14765,11 +15360,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
14765
15360
|
const parseResult = CopilotHookEntrySchema.safeParse(rawEntry);
|
|
14766
15361
|
if (!parseResult.success) continue;
|
|
14767
15362
|
const entry = parseResult.data;
|
|
14768
|
-
const command = resolveImportCommand$1(entry, logger);
|
|
15363
|
+
const { command, shell } = resolveImportCommand$1(entry, logger);
|
|
14769
15364
|
const timeout = entry.timeoutSec;
|
|
14770
15365
|
defs.push({
|
|
14771
15366
|
type: "command",
|
|
14772
15367
|
...command !== void 0 && { command },
|
|
15368
|
+
...shell !== void 0 && { shell },
|
|
15369
|
+
...entry.env !== void 0 && { env: entry.env },
|
|
14773
15370
|
...timeout !== void 0 && { timeout }
|
|
14774
15371
|
});
|
|
14775
15372
|
}
|
|
@@ -15928,6 +16525,27 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
|
|
|
15928
16525
|
const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
|
|
15929
16526
|
const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
|
|
15930
16527
|
/**
|
|
16528
|
+
* Whether an entry of the `hooks:` mapping is a hook-event list rather than one
|
|
16529
|
+
* of its non-event siblings.
|
|
16530
|
+
*
|
|
16531
|
+
* The mapping is not all events: Hermes v0.20.0 nests the outbound webhook
|
|
16532
|
+
* registry there as `hooks.outbound`, a list of targets (`name`, `url`,
|
|
16533
|
+
* `events`, `secret_env`, `matcher`, `timeout`) that rulesync neither authors
|
|
16534
|
+
* nor imports. A documented native event is an event whatever its value; for
|
|
16535
|
+
* anything else the value decides, because rulesync also emits *undocumented*
|
|
16536
|
+
* event names supplied through the `hermesagent.hooks` override (forward
|
|
16537
|
+
* compatibility), and those must stay retractable. Everything rulesync writes
|
|
16538
|
+
* is a non-empty list of entries carrying a string `command`, which no registry
|
|
16539
|
+
* entry has — `outbound` entries carry `url`/`events` instead.
|
|
16540
|
+
*
|
|
16541
|
+
* Both directions ask this one question, so import and generate cannot drift.
|
|
16542
|
+
* @see https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks
|
|
16543
|
+
*/
|
|
16544
|
+
function isHermesHookEventEntry(key, value) {
|
|
16545
|
+
if (HERMESAGENT_NATIVE_EVENTS.has(key)) return true;
|
|
16546
|
+
return Array.isArray(value) && value.length > 0 && value.every((entry) => isPlainObject$1(entry) && typeof entry.command === "string");
|
|
16547
|
+
}
|
|
16548
|
+
/**
|
|
15931
16549
|
* Convert the canonical hooks config into Hermes's native
|
|
15932
16550
|
* `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
|
|
15933
16551
|
*
|
|
@@ -15988,7 +16606,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
15988
16606
|
}
|
|
15989
16607
|
for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
15990
16608
|
if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
|
|
15991
|
-
if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.
|
|
16609
|
+
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.`);
|
|
15992
16610
|
setHermesHookEntries({
|
|
15993
16611
|
result,
|
|
15994
16612
|
event: nativeEvent,
|
|
@@ -16010,6 +16628,7 @@ function hermesHooksToCanonical(hooks) {
|
|
|
16010
16628
|
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
16011
16629
|
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
16012
16630
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
16631
|
+
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
16013
16632
|
const rulesyncEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent] ?? nativeEvent;
|
|
16014
16633
|
const defs = [];
|
|
16015
16634
|
for (const raw of entries) {
|
|
@@ -16029,6 +16648,32 @@ function hermesHooksToCanonical(hooks) {
|
|
|
16029
16648
|
return canonical;
|
|
16030
16649
|
}
|
|
16031
16650
|
/**
|
|
16651
|
+
* Recompute the `hooks:` mapping that is written back to `config.yaml`.
|
|
16652
|
+
*
|
|
16653
|
+
* rulesync owns the hook events inside that mapping, but not the mapping
|
|
16654
|
+
* itself: Hermes v0.20.0 nests the outbound webhook registry under the same key
|
|
16655
|
+
* as `hooks.outbound`, and it is a list of webhook targets rather than a hook
|
|
16656
|
+
* event, so it has no rulesync spelling and no migration path. Replacing the
|
|
16657
|
+
* whole mapping destroyed it on every generate. Every key that is not an event
|
|
16658
|
+
* ({@link isHermesHookEventEntry}) is therefore carried over from the existing
|
|
16659
|
+
* file, while event keys are replaced wholesale so a hook deleted from the
|
|
16660
|
+
* rulesync source is retracted — including one written under an undocumented
|
|
16661
|
+
* event name through the `hermesagent.hooks` override.
|
|
16662
|
+
* @see https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks
|
|
16663
|
+
*/
|
|
16664
|
+
function mergeHermesHooksBlock({ existingHooks, generatedHooks }) {
|
|
16665
|
+
const preserved = {};
|
|
16666
|
+
if (isPlainObject$1(existingHooks)) for (const [key, value] of Object.entries(existingHooks)) {
|
|
16667
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
16668
|
+
if (isHermesHookEventEntry(key, value)) continue;
|
|
16669
|
+
preserved[key] = value;
|
|
16670
|
+
}
|
|
16671
|
+
return {
|
|
16672
|
+
...preserved,
|
|
16673
|
+
...isPlainObject$1(generatedHooks) ? generatedHooks : {}
|
|
16674
|
+
};
|
|
16675
|
+
}
|
|
16676
|
+
/**
|
|
16032
16677
|
* Hermes Agent shell hooks.
|
|
16033
16678
|
*
|
|
16034
16679
|
* Hermes Agent registers shell-command hooks under the `hooks:` key of the
|
|
@@ -16096,14 +16741,22 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
16096
16741
|
return true;
|
|
16097
16742
|
}
|
|
16098
16743
|
setFileContent(fileContent) {
|
|
16744
|
+
const existing = parseSharedConfig({
|
|
16745
|
+
format: "yaml",
|
|
16746
|
+
fileContent
|
|
16747
|
+
});
|
|
16748
|
+
const generated = parseSharedConfig({
|
|
16749
|
+
format: "yaml",
|
|
16750
|
+
fileContent: this.fileContent
|
|
16751
|
+
});
|
|
16099
16752
|
this.fileContent = applySharedConfigPatch({
|
|
16100
16753
|
fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
|
|
16101
16754
|
feature: "hooks",
|
|
16102
16755
|
existingContent: fileContent,
|
|
16103
|
-
patch:
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
})
|
|
16756
|
+
patch: { hooks: mergeHermesHooksBlock({
|
|
16757
|
+
existingHooks: existing.hooks,
|
|
16758
|
+
generatedHooks: generated.hooks
|
|
16759
|
+
}) }
|
|
16107
16760
|
});
|
|
16108
16761
|
}
|
|
16109
16762
|
toRulesyncHooks() {
|
|
@@ -16257,15 +16910,36 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
16257
16910
|
*
|
|
16258
16911
|
* `experimental.session.compacting` receives `(input, output)` and exposes no
|
|
16259
16912
|
* per-invocation identifier worth matching on, so it takes `null`.
|
|
16913
|
+
* `chat.message` receives `(input, output)` with the prompt text living in
|
|
16914
|
+
* `output.parts` rather than a single matchable field, so it takes `null` too.
|
|
16260
16915
|
*
|
|
16261
16916
|
* @see https://opencode.ai/docs/plugins/
|
|
16262
16917
|
*/
|
|
16263
16918
|
const NAMED_HOOK_MATCHER_SUBJECTS = {
|
|
16264
16919
|
"tool.execute.before": "input.tool",
|
|
16265
16920
|
"tool.execute.after": "input.tool",
|
|
16266
|
-
"experimental.session.compacting": null
|
|
16921
|
+
"experimental.session.compacting": null,
|
|
16922
|
+
"chat.message": null
|
|
16267
16923
|
};
|
|
16268
16924
|
/**
|
|
16925
|
+
* Canonical events whose generic (`event.type`) dispatch fires more broadly
|
|
16926
|
+
* than the canonical event means, mapped to the extra condition the generated
|
|
16927
|
+
* handler gates on. Keyed by canonical event like `SHELL_EVENT_TOOL_GATES`, so
|
|
16928
|
+
* a second canonical event mapped onto the same dispatch does not inherit a
|
|
16929
|
+
* gate meant for its sibling.
|
|
16930
|
+
*
|
|
16931
|
+
* `permission.replied` fires for every reply — `once`, `always` and `reject` —
|
|
16932
|
+
* so the canonical `permissionDenied` handler runs only for a rejecting reply.
|
|
16933
|
+
*
|
|
16934
|
+
* Note the v1 SDK's generated `Event` typing still describes this payload as
|
|
16935
|
+
* `{ permissionID, response }`; the schema source, the v2 typings and the TUI's
|
|
16936
|
+
* live consumer all agree on `{ requestID, reply }`, so the stale codegen is
|
|
16937
|
+
* not followed here.
|
|
16938
|
+
*
|
|
16939
|
+
* @see https://opencode.ai/docs/plugins/
|
|
16940
|
+
*/
|
|
16941
|
+
const GENERIC_EVENT_PROPERTY_GATES = { permissionDenied: "event.properties.reply === \"reject\"" };
|
|
16942
|
+
/**
|
|
16269
16943
|
* OpenCode (and Kilo) have no shell-execution lifecycle event — the
|
|
16270
16944
|
* `command.executed` event these canonical events were once mapped to is a
|
|
16271
16945
|
* *slash-command* event, so a hook wired there never fired on bash commands
|
|
@@ -16310,6 +16984,7 @@ function validateAndSanitizeMatcher(matcher) {
|
|
|
16310
16984
|
function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
|
|
16311
16985
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
16312
16986
|
const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
|
|
16987
|
+
const propertyGate = GENERIC_EVENT_PROPERTY_GATES[canonicalEvent];
|
|
16313
16988
|
const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
|
|
16314
16989
|
if (!toolEvent) continue;
|
|
16315
16990
|
const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
|
|
@@ -16321,7 +16996,8 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
|
|
|
16321
16996
|
handlers.push({
|
|
16322
16997
|
command: def.command,
|
|
16323
16998
|
matcher: def.matcher ? def.matcher : void 0,
|
|
16324
|
-
...shellGate ? { toolGate: shellGate.tool } : {}
|
|
16999
|
+
...shellGate ? { toolGate: shellGate.tool } : {},
|
|
17000
|
+
...propertyGate ? { propertyGate } : {}
|
|
16325
17001
|
});
|
|
16326
17002
|
}
|
|
16327
17003
|
if (handlers.length > 0) {
|
|
@@ -16343,7 +17019,11 @@ function buildGenericEventBodyLines(genericEventHandlers) {
|
|
|
16343
17019
|
isFirst = false;
|
|
16344
17020
|
for (const handler of handlers) {
|
|
16345
17021
|
const escapedCommand = escapeForTemplateLiteral(handler.command);
|
|
16346
|
-
|
|
17022
|
+
if (handler.propertyGate) {
|
|
17023
|
+
bodyLines.push(` if (${handler.propertyGate}) {`);
|
|
17024
|
+
bodyLines.push(` await $\`${escapedCommand}\`;`);
|
|
17025
|
+
bodyLines.push(" }");
|
|
17026
|
+
} else bodyLines.push(` await $\`${escapedCommand}\`;`);
|
|
16347
17027
|
}
|
|
16348
17028
|
bodyLines.push(" }");
|
|
16349
17029
|
}
|
|
@@ -16711,13 +17391,246 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
|
|
|
16711
17391
|
}
|
|
16712
17392
|
};
|
|
16713
17393
|
//#endregion
|
|
17394
|
+
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
17395
|
+
/**
|
|
17396
|
+
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
17397
|
+
*
|
|
17398
|
+
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
17399
|
+
* imports do not drop data they do not yet understand.
|
|
17400
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
17401
|
+
*/
|
|
17402
|
+
const KiroIdeHookActionSchema = z.union([z.looseObject({
|
|
17403
|
+
type: z.literal("command"),
|
|
17404
|
+
command: z.optional(safeString)
|
|
17405
|
+
}), z.looseObject({
|
|
17406
|
+
type: z.literal("agent"),
|
|
17407
|
+
prompt: z.optional(safeString)
|
|
17408
|
+
})]);
|
|
17409
|
+
const KiroIdeHookEntrySchema = z.looseObject({
|
|
17410
|
+
name: z.optional(z.string()),
|
|
17411
|
+
description: z.optional(z.string()),
|
|
17412
|
+
trigger: z.optional(z.string()),
|
|
17413
|
+
matcher: z.optional(z.string()),
|
|
17414
|
+
action: z.optional(KiroIdeHookActionSchema),
|
|
17415
|
+
timeout: z.optional(z.number()),
|
|
17416
|
+
enabled: z.optional(z.boolean())
|
|
17417
|
+
});
|
|
17418
|
+
const KiroIdeHooksFileSchema = z.looseObject({
|
|
17419
|
+
version: z.optional(z.string()),
|
|
17420
|
+
hooks: z.optional(z.array(KiroIdeHookEntrySchema))
|
|
17421
|
+
});
|
|
17422
|
+
/**
|
|
17423
|
+
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
17424
|
+
*
|
|
17425
|
+
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
17426
|
+
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
17427
|
+
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
17428
|
+
* already warns about unsupported types).
|
|
17429
|
+
*/
|
|
17430
|
+
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
17431
|
+
const entries = [];
|
|
17432
|
+
for (const def of definitions) {
|
|
17433
|
+
const type = def.type ?? "command";
|
|
17434
|
+
let action;
|
|
17435
|
+
if (type === "command") {
|
|
17436
|
+
if (def.command === void 0) continue;
|
|
17437
|
+
action = {
|
|
17438
|
+
type: "command",
|
|
17439
|
+
command: def.command
|
|
17440
|
+
};
|
|
17441
|
+
} else if (type === "prompt") {
|
|
17442
|
+
if (def.prompt === void 0) continue;
|
|
17443
|
+
action = {
|
|
17444
|
+
type: "agent",
|
|
17445
|
+
prompt: def.prompt
|
|
17446
|
+
};
|
|
17447
|
+
} else continue;
|
|
17448
|
+
entries.push({
|
|
17449
|
+
name: def.name ?? trigger,
|
|
17450
|
+
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
17451
|
+
trigger,
|
|
17452
|
+
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
17453
|
+
action,
|
|
17454
|
+
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
17455
|
+
enabled: def.enabled ?? true
|
|
17456
|
+
});
|
|
17457
|
+
}
|
|
17458
|
+
return entries;
|
|
17459
|
+
}
|
|
17460
|
+
function canonicalToKiroIdeHooks(config, overrideKey) {
|
|
17461
|
+
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
17462
|
+
const sharedHooks = {};
|
|
17463
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
17464
|
+
const effectiveHooks = {
|
|
17465
|
+
...sharedHooks,
|
|
17466
|
+
...config[overrideKey]?.hooks
|
|
17467
|
+
};
|
|
17468
|
+
const entries = [];
|
|
17469
|
+
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
17470
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
17471
|
+
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
17472
|
+
}
|
|
17473
|
+
return entries;
|
|
17474
|
+
}
|
|
17475
|
+
function kiroIdeHooksToCanonical(entries) {
|
|
17476
|
+
const canonical = {};
|
|
17477
|
+
for (const entry of entries) {
|
|
17478
|
+
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
17479
|
+
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
17480
|
+
if (isPrototypePollutionKey(eventName)) continue;
|
|
17481
|
+
const def = {};
|
|
17482
|
+
if (entry.action.type === "command") {
|
|
17483
|
+
if (!entry.action.command) continue;
|
|
17484
|
+
def.type = "command";
|
|
17485
|
+
def.command = entry.action.command;
|
|
17486
|
+
} else {
|
|
17487
|
+
if (!entry.action.prompt) continue;
|
|
17488
|
+
def.type = "prompt";
|
|
17489
|
+
def.prompt = entry.action.prompt;
|
|
17490
|
+
}
|
|
17491
|
+
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
17492
|
+
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
17493
|
+
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
17494
|
+
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
17495
|
+
if (entry.enabled === false) def.enabled = false;
|
|
17496
|
+
(canonical[eventName] ??= []).push(def);
|
|
17497
|
+
}
|
|
17498
|
+
return canonical;
|
|
17499
|
+
}
|
|
17500
|
+
/**
|
|
17501
|
+
* Hooks generator for the standalone Kiro hooks format (`.kiro/hooks/*.json`
|
|
17502
|
+
* v1), used by the **Kiro IDE** and, since Kiro CLI 3.0, by the CLI too.
|
|
17503
|
+
*
|
|
17504
|
+
* Kiro reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
17505
|
+
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
17506
|
+
* `hooks` array, so rulesync emits every generated hook into one
|
|
17507
|
+
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
17508
|
+
* which keeps it within the single-file hooks architecture.
|
|
17509
|
+
*
|
|
17510
|
+
* {@link import("./kiro-cli-hooks.js").KiroCliHooks} subclasses this to write
|
|
17511
|
+
* the same format for the `kiro-cli` target; only the deprecated `kiro` alias
|
|
17512
|
+
* still writes the embedded `.kiro/agents/default.json` agent-config shape,
|
|
17513
|
+
* which Kiro CLI 3.0 no longer reads.
|
|
17514
|
+
*
|
|
17515
|
+
* @see https://kiro.dev/docs/hooks/
|
|
17516
|
+
*/
|
|
17517
|
+
var KiroIdeHooks = class extends ToolHooks {
|
|
17518
|
+
constructor(params) {
|
|
17519
|
+
super({
|
|
17520
|
+
...params,
|
|
17521
|
+
fileContent: params.fileContent ?? JSON.stringify({
|
|
17522
|
+
version: "v1",
|
|
17523
|
+
hooks: []
|
|
17524
|
+
}, null, 2)
|
|
17525
|
+
});
|
|
17526
|
+
}
|
|
17527
|
+
/**
|
|
17528
|
+
* The `HooksConfig` key whose `hooks` block provides tool-specific overrides
|
|
17529
|
+
* for this target. {@link import("./kiro-cli-hooks.js").KiroCliHooks}
|
|
17530
|
+
* overrides this to `kiro-cli`.
|
|
17531
|
+
*/
|
|
17532
|
+
static getOverrideKey() {
|
|
17533
|
+
return "kiro-ide";
|
|
17534
|
+
}
|
|
17535
|
+
static getSettablePaths(_options = {}) {
|
|
17536
|
+
return {
|
|
17537
|
+
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
17538
|
+
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
17539
|
+
};
|
|
17540
|
+
}
|
|
17541
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
17542
|
+
const paths = this.getSettablePaths({ global });
|
|
17543
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
17544
|
+
version: "v1",
|
|
17545
|
+
hooks: []
|
|
17546
|
+
}, null, 2);
|
|
17547
|
+
return new this({
|
|
17548
|
+
outputRoot,
|
|
17549
|
+
relativeDirPath: paths.relativeDirPath,
|
|
17550
|
+
relativeFilePath: paths.relativeFilePath,
|
|
17551
|
+
fileContent,
|
|
17552
|
+
validate
|
|
17553
|
+
});
|
|
17554
|
+
}
|
|
17555
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
17556
|
+
const paths = this.getSettablePaths({ global });
|
|
17557
|
+
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson(), this.getOverrideKey());
|
|
17558
|
+
const fileContent = JSON.stringify({
|
|
17559
|
+
version: "v1",
|
|
17560
|
+
hooks
|
|
17561
|
+
}, null, 2);
|
|
17562
|
+
return new this({
|
|
17563
|
+
outputRoot,
|
|
17564
|
+
relativeDirPath: paths.relativeDirPath,
|
|
17565
|
+
relativeFilePath: paths.relativeFilePath,
|
|
17566
|
+
fileContent,
|
|
17567
|
+
validate
|
|
17568
|
+
});
|
|
17569
|
+
}
|
|
17570
|
+
toRulesyncHooks() {
|
|
17571
|
+
let parsed;
|
|
17572
|
+
try {
|
|
17573
|
+
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
17574
|
+
} catch (error) {
|
|
17575
|
+
throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17576
|
+
}
|
|
17577
|
+
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
17578
|
+
const overrideKey = this.constructor.getOverrideKey();
|
|
17579
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
17580
|
+
hooks,
|
|
17581
|
+
overrideKey
|
|
17582
|
+
}), null, 2) });
|
|
17583
|
+
}
|
|
17584
|
+
validate() {
|
|
17585
|
+
return {
|
|
17586
|
+
success: true,
|
|
17587
|
+
error: null
|
|
17588
|
+
};
|
|
17589
|
+
}
|
|
17590
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
17591
|
+
return new this({
|
|
17592
|
+
outputRoot,
|
|
17593
|
+
relativeDirPath,
|
|
17594
|
+
relativeFilePath,
|
|
17595
|
+
fileContent: JSON.stringify({
|
|
17596
|
+
version: "v1",
|
|
17597
|
+
hooks: []
|
|
17598
|
+
}, null, 2),
|
|
17599
|
+
validate: false
|
|
17600
|
+
});
|
|
17601
|
+
}
|
|
17602
|
+
};
|
|
17603
|
+
//#endregion
|
|
17604
|
+
//#region src/features/hooks/kiro-cli-hooks.ts
|
|
17605
|
+
/**
|
|
17606
|
+
* Hooks generator for the **Kiro CLI**.
|
|
17607
|
+
*
|
|
17608
|
+
* Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
|
|
17609
|
+
* Kiro IDE reads, so this reuses {@link KiroIdeHooks} and only redirects the
|
|
17610
|
+
* tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
17611
|
+
* the rulesync hooks config are honored, rather than `kiro-ide.hooks`).
|
|
17612
|
+
*
|
|
17613
|
+
* The embedded `.kiro/agents/default.json` agent-hook format this target used
|
|
17614
|
+
* to emit is documented as not working in 3.0, so it is left to the deprecated
|
|
17615
|
+
* `kiro` alias ({@link import("./kiro-hooks.js").KiroHooks}).
|
|
17616
|
+
*
|
|
17617
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
17618
|
+
* @see https://kiro.dev/docs/hooks/
|
|
17619
|
+
*/
|
|
17620
|
+
var KiroCliHooks = class extends KiroIdeHooks {
|
|
17621
|
+
static getOverrideKey() {
|
|
17622
|
+
return "kiro-cli";
|
|
17623
|
+
}
|
|
17624
|
+
};
|
|
17625
|
+
//#endregion
|
|
16714
17626
|
//#region src/features/hooks/kiro-hooks.ts
|
|
16715
17627
|
/**
|
|
16716
|
-
* Convert canonical hooks config to Kiro
|
|
17628
|
+
* Convert canonical hooks config to the legacy embedded Kiro agent-config
|
|
17629
|
+
* format.
|
|
16717
17630
|
* Filters shared hooks to KIRO_HOOK_EVENTS, merges config.kiro?.hooks,
|
|
16718
|
-
* then maps event names and emits
|
|
17631
|
+
* then maps event names and emits the agent config's hook arrays.
|
|
16719
17632
|
*/
|
|
16720
|
-
/** Build the
|
|
17633
|
+
/** Build the agent-config hook entries for a single canonical event's definitions. */
|
|
16721
17634
|
function buildKiroEntriesForEvent(definitions) {
|
|
16722
17635
|
const entries = [];
|
|
16723
17636
|
for (const def of definitions) {
|
|
@@ -16733,7 +17646,8 @@ function buildKiroEntriesForEvent(definitions) {
|
|
|
16733
17646
|
}
|
|
16734
17647
|
return entries;
|
|
16735
17648
|
}
|
|
16736
|
-
function canonicalToKiroHooks(config
|
|
17649
|
+
function canonicalToKiroHooks(config) {
|
|
17650
|
+
const overrideKey = "kiro";
|
|
16737
17651
|
const kiroSupported = new Set(KIRO_HOOK_EVENTS);
|
|
16738
17652
|
const sharedHooks = {};
|
|
16739
17653
|
for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
|
|
@@ -16751,8 +17665,8 @@ function canonicalToKiroHooks(config, overrideKey = "kiro") {
|
|
|
16751
17665
|
return kiro;
|
|
16752
17666
|
}
|
|
16753
17667
|
/**
|
|
16754
|
-
*
|
|
16755
|
-
* Uses `z.looseObject` so that unknown fields added by future Kiro
|
|
17668
|
+
* Hook entry as stored in each event's array of the agent config.
|
|
17669
|
+
* Uses `z.looseObject` so that unknown fields added by future Kiro
|
|
16756
17670
|
* versions are accepted and silently ignored during import.
|
|
16757
17671
|
*/
|
|
16758
17672
|
const KiroHookEntrySchema = z.looseObject({
|
|
@@ -16768,7 +17682,7 @@ function importCacheTtl(entry) {
|
|
|
16768
17682
|
return { cacheTtl: entry.cache_ttl_seconds };
|
|
16769
17683
|
}
|
|
16770
17684
|
/**
|
|
16771
|
-
* Extract hooks from Kiro
|
|
17685
|
+
* Extract hooks from the Kiro agent config into canonical format.
|
|
16772
17686
|
*/
|
|
16773
17687
|
function kiroHooksToCanonical(kiroHooks) {
|
|
16774
17688
|
if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
|
|
@@ -16796,6 +17710,17 @@ function kiroHooksToCanonical(kiroHooks) {
|
|
|
16796
17710
|
}
|
|
16797
17711
|
return canonical;
|
|
16798
17712
|
}
|
|
17713
|
+
/**
|
|
17714
|
+
* Hooks generator for the deprecated `kiro` alias: the embedded hook block of
|
|
17715
|
+
* `.kiro/agents/default.json`.
|
|
17716
|
+
*
|
|
17717
|
+
* Kiro's hooks migration guide states this format "does not work in 3.0", so
|
|
17718
|
+
* the `kiro-cli` target writes the standalone `.kiro/hooks/*.json` v1 format
|
|
17719
|
+
* instead ({@link import("./kiro-cli-hooks.js").KiroCliHooks}). It is kept here
|
|
17720
|
+
* so an existing agent config still round-trips.
|
|
17721
|
+
*
|
|
17722
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
17723
|
+
*/
|
|
16799
17724
|
var KiroHooks = class KiroHooks extends ToolHooks {
|
|
16800
17725
|
constructor(params) {
|
|
16801
17726
|
super({
|
|
@@ -16803,14 +17728,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16803
17728
|
fileContent: params.fileContent ?? "{}"
|
|
16804
17729
|
});
|
|
16805
17730
|
}
|
|
16806
|
-
/**
|
|
16807
|
-
* The `HooksConfig` key whose `hooks` block provides tool-specific overrides
|
|
16808
|
-
* for this target. The legacy `kiro` alias uses `kiro`; {@link import(
|
|
16809
|
-
* "./kiro-cli-hooks.js").KiroCliHooks} overrides this to `kiro-cli`.
|
|
16810
|
-
*/
|
|
16811
|
-
static getOverrideKey() {
|
|
16812
|
-
return "kiro";
|
|
16813
|
-
}
|
|
16814
17731
|
isDeletable() {
|
|
16815
17732
|
return false;
|
|
16816
17733
|
}
|
|
@@ -16835,7 +17752,7 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16835
17752
|
const paths = KiroHooks.getSettablePaths({ global });
|
|
16836
17753
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
16837
17754
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
16838
|
-
const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson()
|
|
17755
|
+
const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson());
|
|
16839
17756
|
const fileContent = applySharedConfigPatch({
|
|
16840
17757
|
fileKey: sharedConfigFileKey(paths),
|
|
16841
17758
|
feature: "hooks",
|
|
@@ -16859,10 +17776,9 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16859
17776
|
throw new Error(`Failed to parse Kiro hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
16860
17777
|
}
|
|
16861
17778
|
const hooks = kiroHooksToCanonical(agentConfig.hooks);
|
|
16862
|
-
const overrideKey = this.constructor.getOverrideKey();
|
|
16863
17779
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
16864
17780
|
hooks,
|
|
16865
|
-
overrideKey
|
|
17781
|
+
overrideKey: "kiro"
|
|
16866
17782
|
}), null, 2) });
|
|
16867
17783
|
}
|
|
16868
17784
|
validate() {
|
|
@@ -16882,222 +17798,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
16882
17798
|
}
|
|
16883
17799
|
};
|
|
16884
17800
|
//#endregion
|
|
16885
|
-
//#region src/features/hooks/kiro-cli-hooks.ts
|
|
16886
|
-
/**
|
|
16887
|
-
* Hooks generator for the **Kiro CLI**.
|
|
16888
|
-
*
|
|
16889
|
-
* The Kiro CLI uses the same `.kiro/agents/default.json` agent-hook format as
|
|
16890
|
-
* the legacy `kiro` alias, so this reuses {@link KiroHooks} and only redirects
|
|
16891
|
-
* the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
16892
|
-
* the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
|
|
16893
|
-
*
|
|
16894
|
-
* (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
|
|
16895
|
-
* {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
|
|
16896
|
-
*/
|
|
16897
|
-
var KiroCliHooks = class extends KiroHooks {
|
|
16898
|
-
static getOverrideKey() {
|
|
16899
|
-
return "kiro-cli";
|
|
16900
|
-
}
|
|
16901
|
-
};
|
|
16902
|
-
//#endregion
|
|
16903
|
-
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
16904
|
-
/**
|
|
16905
|
-
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
16906
|
-
*
|
|
16907
|
-
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
16908
|
-
* imports do not drop data they do not yet understand.
|
|
16909
|
-
* @see https://kiro.dev/docs/hooks/types/
|
|
16910
|
-
*/
|
|
16911
|
-
const KiroIdeHookActionSchema = z.union([z.looseObject({
|
|
16912
|
-
type: z.literal("command"),
|
|
16913
|
-
command: z.optional(safeString)
|
|
16914
|
-
}), z.looseObject({
|
|
16915
|
-
type: z.literal("agent"),
|
|
16916
|
-
prompt: z.optional(safeString)
|
|
16917
|
-
})]);
|
|
16918
|
-
const KiroIdeHookEntrySchema = z.looseObject({
|
|
16919
|
-
name: z.optional(z.string()),
|
|
16920
|
-
description: z.optional(z.string()),
|
|
16921
|
-
trigger: z.optional(z.string()),
|
|
16922
|
-
matcher: z.optional(z.string()),
|
|
16923
|
-
action: z.optional(KiroIdeHookActionSchema),
|
|
16924
|
-
timeout: z.optional(z.number()),
|
|
16925
|
-
enabled: z.optional(z.boolean())
|
|
16926
|
-
});
|
|
16927
|
-
const KiroIdeHooksFileSchema = z.looseObject({
|
|
16928
|
-
version: z.optional(z.string()),
|
|
16929
|
-
hooks: z.optional(z.array(KiroIdeHookEntrySchema))
|
|
16930
|
-
});
|
|
16931
|
-
/**
|
|
16932
|
-
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
16933
|
-
*
|
|
16934
|
-
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
16935
|
-
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
16936
|
-
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
16937
|
-
* already warns about unsupported types).
|
|
16938
|
-
*/
|
|
16939
|
-
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
16940
|
-
const entries = [];
|
|
16941
|
-
for (const def of definitions) {
|
|
16942
|
-
const type = def.type ?? "command";
|
|
16943
|
-
let action;
|
|
16944
|
-
if (type === "command") {
|
|
16945
|
-
if (def.command === void 0) continue;
|
|
16946
|
-
action = {
|
|
16947
|
-
type: "command",
|
|
16948
|
-
command: def.command
|
|
16949
|
-
};
|
|
16950
|
-
} else if (type === "prompt") {
|
|
16951
|
-
if (def.prompt === void 0) continue;
|
|
16952
|
-
action = {
|
|
16953
|
-
type: "agent",
|
|
16954
|
-
prompt: def.prompt
|
|
16955
|
-
};
|
|
16956
|
-
} else continue;
|
|
16957
|
-
entries.push({
|
|
16958
|
-
name: def.name ?? trigger,
|
|
16959
|
-
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
16960
|
-
trigger,
|
|
16961
|
-
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
16962
|
-
action,
|
|
16963
|
-
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
16964
|
-
enabled: def.enabled ?? true
|
|
16965
|
-
});
|
|
16966
|
-
}
|
|
16967
|
-
return entries;
|
|
16968
|
-
}
|
|
16969
|
-
function canonicalToKiroIdeHooks(config) {
|
|
16970
|
-
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
16971
|
-
const sharedHooks = {};
|
|
16972
|
-
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
16973
|
-
const effectiveHooks = {
|
|
16974
|
-
...sharedHooks,
|
|
16975
|
-
...config["kiro-ide"]?.hooks
|
|
16976
|
-
};
|
|
16977
|
-
const entries = [];
|
|
16978
|
-
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
16979
|
-
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
16980
|
-
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
16981
|
-
}
|
|
16982
|
-
return entries;
|
|
16983
|
-
}
|
|
16984
|
-
function kiroIdeHooksToCanonical(entries) {
|
|
16985
|
-
const canonical = {};
|
|
16986
|
-
for (const entry of entries) {
|
|
16987
|
-
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
16988
|
-
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
16989
|
-
if (isPrototypePollutionKey(eventName)) continue;
|
|
16990
|
-
const def = {};
|
|
16991
|
-
if (entry.action.type === "command") {
|
|
16992
|
-
if (!entry.action.command) continue;
|
|
16993
|
-
def.type = "command";
|
|
16994
|
-
def.command = entry.action.command;
|
|
16995
|
-
} else {
|
|
16996
|
-
if (!entry.action.prompt) continue;
|
|
16997
|
-
def.type = "prompt";
|
|
16998
|
-
def.prompt = entry.action.prompt;
|
|
16999
|
-
}
|
|
17000
|
-
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
17001
|
-
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
17002
|
-
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
17003
|
-
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
17004
|
-
if (entry.enabled === false) def.enabled = false;
|
|
17005
|
-
(canonical[eventName] ??= []).push(def);
|
|
17006
|
-
}
|
|
17007
|
-
return canonical;
|
|
17008
|
-
}
|
|
17009
|
-
/**
|
|
17010
|
-
* Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
|
|
17011
|
-
*
|
|
17012
|
-
* Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
17013
|
-
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
17014
|
-
* `hooks` array, so rulesync emits every generated hook into one
|
|
17015
|
-
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
17016
|
-
* which keeps it within the single-file hooks architecture.
|
|
17017
|
-
*
|
|
17018
|
-
* This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
|
|
17019
|
-
* KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
|
|
17020
|
-
*
|
|
17021
|
-
* @see https://kiro.dev/docs/hooks/
|
|
17022
|
-
*/
|
|
17023
|
-
var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
|
|
17024
|
-
constructor(params) {
|
|
17025
|
-
super({
|
|
17026
|
-
...params,
|
|
17027
|
-
fileContent: params.fileContent ?? JSON.stringify({
|
|
17028
|
-
version: "v1",
|
|
17029
|
-
hooks: []
|
|
17030
|
-
}, null, 2)
|
|
17031
|
-
});
|
|
17032
|
-
}
|
|
17033
|
-
static getSettablePaths(_options = {}) {
|
|
17034
|
-
return {
|
|
17035
|
-
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
17036
|
-
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
17037
|
-
};
|
|
17038
|
-
}
|
|
17039
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
17040
|
-
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
17041
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
17042
|
-
version: "v1",
|
|
17043
|
-
hooks: []
|
|
17044
|
-
}, null, 2);
|
|
17045
|
-
return new KiroIdeHooks({
|
|
17046
|
-
outputRoot,
|
|
17047
|
-
relativeDirPath: paths.relativeDirPath,
|
|
17048
|
-
relativeFilePath: paths.relativeFilePath,
|
|
17049
|
-
fileContent,
|
|
17050
|
-
validate
|
|
17051
|
-
});
|
|
17052
|
-
}
|
|
17053
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
17054
|
-
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
17055
|
-
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
|
|
17056
|
-
const fileContent = JSON.stringify({
|
|
17057
|
-
version: "v1",
|
|
17058
|
-
hooks
|
|
17059
|
-
}, null, 2);
|
|
17060
|
-
return new KiroIdeHooks({
|
|
17061
|
-
outputRoot,
|
|
17062
|
-
relativeDirPath: paths.relativeDirPath,
|
|
17063
|
-
relativeFilePath: paths.relativeFilePath,
|
|
17064
|
-
fileContent,
|
|
17065
|
-
validate
|
|
17066
|
-
});
|
|
17067
|
-
}
|
|
17068
|
-
toRulesyncHooks() {
|
|
17069
|
-
let parsed;
|
|
17070
|
-
try {
|
|
17071
|
-
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
17072
|
-
} catch (error) {
|
|
17073
|
-
throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17074
|
-
}
|
|
17075
|
-
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
17076
|
-
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
17077
|
-
hooks,
|
|
17078
|
-
overrideKey: "kiro-ide"
|
|
17079
|
-
}), null, 2) });
|
|
17080
|
-
}
|
|
17081
|
-
validate() {
|
|
17082
|
-
return {
|
|
17083
|
-
success: true,
|
|
17084
|
-
error: null
|
|
17085
|
-
};
|
|
17086
|
-
}
|
|
17087
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
17088
|
-
return new KiroIdeHooks({
|
|
17089
|
-
outputRoot,
|
|
17090
|
-
relativeDirPath,
|
|
17091
|
-
relativeFilePath,
|
|
17092
|
-
fileContent: JSON.stringify({
|
|
17093
|
-
version: "v1",
|
|
17094
|
-
hooks: []
|
|
17095
|
-
}, null, 2),
|
|
17096
|
-
validate: false
|
|
17097
|
-
});
|
|
17098
|
-
}
|
|
17099
|
-
};
|
|
17100
|
-
//#endregion
|
|
17101
17801
|
//#region src/features/hooks/opencode-hooks.ts
|
|
17102
17802
|
var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
17103
17803
|
constructor(params) {
|
|
@@ -18161,6 +18861,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
18161
18861
|
supportedHookTypes: ["command"],
|
|
18162
18862
|
supportsMatcher: true
|
|
18163
18863
|
}],
|
|
18864
|
+
["cline", {
|
|
18865
|
+
class: ClineHooks,
|
|
18866
|
+
meta: {
|
|
18867
|
+
supportsProject: true,
|
|
18868
|
+
supportsGlobal: true,
|
|
18869
|
+
supportsImport: false
|
|
18870
|
+
},
|
|
18871
|
+
supportedEvents: CLINE_HOOK_EVENTS,
|
|
18872
|
+
supportedHookTypes: ["command"],
|
|
18873
|
+
supportsMatcher: false
|
|
18874
|
+
}],
|
|
18164
18875
|
["goose", {
|
|
18165
18876
|
class: GooseHooks,
|
|
18166
18877
|
meta: {
|
|
@@ -18222,12 +18933,13 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
18222
18933
|
class: KiroCliHooks,
|
|
18223
18934
|
meta: {
|
|
18224
18935
|
supportsProject: true,
|
|
18225
|
-
supportsGlobal:
|
|
18936
|
+
supportsGlobal: true,
|
|
18226
18937
|
supportsImport: true
|
|
18227
18938
|
},
|
|
18228
|
-
supportedEvents:
|
|
18229
|
-
supportedHookTypes: ["command"],
|
|
18230
|
-
supportsMatcher: true
|
|
18939
|
+
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
18940
|
+
supportedHookTypes: ["command", "prompt"],
|
|
18941
|
+
supportsMatcher: true,
|
|
18942
|
+
passthroughOverrideEvents: true
|
|
18231
18943
|
}],
|
|
18232
18944
|
["kiro-ide", {
|
|
18233
18945
|
class: KiroIdeHooks,
|
|
@@ -18366,7 +19078,11 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
18366
19078
|
relativeFilePath: paths.relativeFilePath,
|
|
18367
19079
|
global: this.global
|
|
18368
19080
|
});
|
|
18369
|
-
const
|
|
19081
|
+
const auxiliaryFiles = await factory.class.getDeletableAuxiliaryFiles?.({
|
|
19082
|
+
outputRoot: this.outputRoot,
|
|
19083
|
+
global: this.global
|
|
19084
|
+
}) ?? [];
|
|
19085
|
+
const list = [...toolHooks.isDeletable?.() !== false ? [toolHooks] : [], ...auxiliaryFiles.filter((file) => file.isDeletable())];
|
|
18370
19086
|
this.logger.debug(`Successfully loaded ${list.length} ${this.toolTarget} hooks files for deletion`);
|
|
18371
19087
|
return list;
|
|
18372
19088
|
}
|
|
@@ -18431,7 +19147,7 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
18431
19147
|
effectiveHooks
|
|
18432
19148
|
});
|
|
18433
19149
|
if (eventsWithUnsupportedMatcher.length > 0) this.logger.warn(`Skipped matcher hook(s) for ${this.toolTarget} (not supported): ${eventsWithUnsupportedMatcher.join(", ")}`);
|
|
18434
|
-
const
|
|
19150
|
+
const toolHooks = await factory.class.fromRulesyncHooks({
|
|
18435
19151
|
outputRoot: this.outputRoot,
|
|
18436
19152
|
rulesyncHooks,
|
|
18437
19153
|
validate: true,
|
|
@@ -18440,10 +19156,16 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
18440
19156
|
logger: this.logger,
|
|
18441
19157
|
toolTarget: this.toolTarget
|
|
18442
19158
|
})
|
|
18443
|
-
})
|
|
19159
|
+
});
|
|
19160
|
+
const result = [toolHooks];
|
|
18444
19161
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
18445
19162
|
outputRoot: this.outputRoot,
|
|
18446
|
-
global: this.global
|
|
19163
|
+
global: this.global,
|
|
19164
|
+
toolHooks,
|
|
19165
|
+
logger: withToolTargetPrefix({
|
|
19166
|
+
logger: this.logger,
|
|
19167
|
+
toolTarget: this.toolTarget
|
|
19168
|
+
})
|
|
18447
19169
|
});
|
|
18448
19170
|
if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
|
|
18449
19171
|
return result;
|
|
@@ -18921,29 +19643,43 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
|
|
|
18921
19643
|
* `.codeiumignore` filename is read as a fallback so existing projects still
|
|
18922
19644
|
* round-trip.
|
|
18923
19645
|
*
|
|
19646
|
+
* In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
|
|
19647
|
+
* instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
|
|
19648
|
+
* legacy brand spelling and sits outside `~/.config/devin`.
|
|
19649
|
+
*
|
|
18924
19650
|
* @see https://docs.devin.ai/desktop/changelog — v3.1.7 added `.devinignore`
|
|
18925
19651
|
* alongside `.windsurfignore` and `.codeiumignore`.
|
|
18926
19652
|
*/
|
|
18927
19653
|
var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
18928
|
-
static getSettablePaths() {
|
|
19654
|
+
static getSettablePaths({ global = false } = {}) {
|
|
18929
19655
|
return {
|
|
18930
|
-
relativeDirPath: ".",
|
|
18931
|
-
relativeFilePath: DEVIN_IGNORE_FILE_NAME
|
|
19656
|
+
relativeDirPath: global ? DEVIN_GLOBAL_IGNORE_DIR_PATH : ".",
|
|
19657
|
+
relativeFilePath: global ? DEVIN_GLOBAL_IGNORE_FILE_NAME : DEVIN_IGNORE_FILE_NAME
|
|
18932
19658
|
};
|
|
18933
19659
|
}
|
|
18934
19660
|
toRulesyncIgnore() {
|
|
18935
19661
|
return this.toRulesyncIgnoreDefault();
|
|
18936
19662
|
}
|
|
18937
|
-
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
19663
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
19664
|
+
const paths = this.getSettablePaths({ global });
|
|
18938
19665
|
return new DevinIgnore({
|
|
18939
19666
|
outputRoot,
|
|
18940
|
-
relativeDirPath:
|
|
18941
|
-
relativeFilePath:
|
|
18942
|
-
fileContent: rulesyncIgnore.getFileContent()
|
|
19667
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19668
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19669
|
+
fileContent: rulesyncIgnore.getFileContent(),
|
|
19670
|
+
global
|
|
18943
19671
|
});
|
|
18944
19672
|
}
|
|
18945
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
18946
|
-
const { relativeDirPath, relativeFilePath } = this.getSettablePaths();
|
|
19673
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
19674
|
+
const { relativeDirPath, relativeFilePath } = this.getSettablePaths({ global });
|
|
19675
|
+
if (global) return new DevinIgnore({
|
|
19676
|
+
outputRoot,
|
|
19677
|
+
relativeDirPath,
|
|
19678
|
+
relativeFilePath,
|
|
19679
|
+
fileContent: await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath)),
|
|
19680
|
+
validate,
|
|
19681
|
+
global
|
|
19682
|
+
});
|
|
18947
19683
|
const primaryPath = join(outputRoot, relativeDirPath, relativeFilePath);
|
|
18948
19684
|
const legacyPath = join(outputRoot, relativeDirPath, DEVIN_LEGACY_IGNORE_FILE_NAME);
|
|
18949
19685
|
const resolvedFilePath = !await fileExists(primaryPath) && await fileExists(legacyPath) ? DEVIN_LEGACY_IGNORE_FILE_NAME : relativeFilePath;
|
|
@@ -18953,16 +19689,18 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
18953
19689
|
relativeDirPath,
|
|
18954
19690
|
relativeFilePath: resolvedFilePath,
|
|
18955
19691
|
fileContent,
|
|
18956
|
-
validate
|
|
19692
|
+
validate,
|
|
19693
|
+
global
|
|
18957
19694
|
});
|
|
18958
19695
|
}
|
|
18959
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
19696
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
18960
19697
|
return new DevinIgnore({
|
|
18961
19698
|
outputRoot,
|
|
18962
19699
|
relativeDirPath,
|
|
18963
19700
|
relativeFilePath,
|
|
18964
19701
|
fileContent: "",
|
|
18965
|
-
validate: false
|
|
19702
|
+
validate: false,
|
|
19703
|
+
global
|
|
18966
19704
|
});
|
|
18967
19705
|
}
|
|
18968
19706
|
};
|
|
@@ -19836,6 +20574,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
19836
20574
|
]);
|
|
19837
20575
|
const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
|
|
19838
20576
|
const ignoreProcessorGlobalToolTargets = [
|
|
20577
|
+
"devin",
|
|
19839
20578
|
"kiro",
|
|
19840
20579
|
"kiro-cli",
|
|
19841
20580
|
"kiro-ide",
|
|
@@ -20763,6 +21502,65 @@ const RULESYNC_TO_CODEX_SCALAR_FIELD_MAP = { experimentalEnvironment: "experimen
|
|
|
20763
21502
|
const CODEX_TO_RULESYNC_SCALAR_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
|
|
20764
21503
|
const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
20765
21504
|
/**
|
|
21505
|
+
* Canonical per-server keys Codex has no counterpart for.
|
|
21506
|
+
*
|
|
21507
|
+
* Codex's deserializer (`RawMcpServerConfig`) does not reject unknown keys, so
|
|
21508
|
+
* these are inert rather than fatal — but they are rulesync's own spellings and
|
|
21509
|
+
* only add noise to a hand-edited `config.toml`. `type`/`transport` are safe to
|
|
21510
|
+
* drop because Codex infers the transport from `command` versus `url`.
|
|
21511
|
+
* `tools` is handled separately: it is fatal rather than inert.
|
|
21512
|
+
* @see https://github.com/openai/codex/blob/rust-v0.146.1/codex-rs/config/src/mcp_types.rs
|
|
21513
|
+
*/
|
|
21514
|
+
const CODEX_UNSUPPORTED_CANONICAL_KEYS = /* @__PURE__ */ new Set([
|
|
21515
|
+
"type",
|
|
21516
|
+
"transport",
|
|
21517
|
+
"alwaysAllow",
|
|
21518
|
+
"trust",
|
|
21519
|
+
"kiroAutoApprove",
|
|
21520
|
+
"kiroAutoBlock"
|
|
21521
|
+
]);
|
|
21522
|
+
/**
|
|
21523
|
+
* Canonical millisecond timeouts and the Codex fields they translate to.
|
|
21524
|
+
* Codex takes both as seconds (`f64`), so the value is divided by 1000 and a
|
|
21525
|
+
* fractional result is emitted as-is.
|
|
21526
|
+
* - `timeout` → `tool_timeout_sec`: default timeout for tool calls on the server.
|
|
21527
|
+
* - `networkTimeout` → `startup_timeout_sec`: initialize + list-tools timeout.
|
|
21528
|
+
*/
|
|
21529
|
+
const RULESYNC_TO_CODEX_TIMEOUT_FIELD_MAP = {
|
|
21530
|
+
timeout: "tool_timeout_sec",
|
|
21531
|
+
networkTimeout: "startup_timeout_sec"
|
|
21532
|
+
};
|
|
21533
|
+
const CODEX_TO_RULESYNC_TIMEOUT_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_TIMEOUT_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
|
|
21534
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
21535
|
+
/**
|
|
21536
|
+
* Whether a value is usable as a timeout. Codex builds a `Duration` out of both
|
|
21537
|
+
* timeout fields, and `Duration::try_from_secs_f64` errors on a negative value —
|
|
21538
|
+
* which fails the whole `config.toml`, not just the one server — so a negative
|
|
21539
|
+
* timeout is rejected here rather than written.
|
|
21540
|
+
*/
|
|
21541
|
+
function isTimeoutValue(value) {
|
|
21542
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
21543
|
+
}
|
|
21544
|
+
/**
|
|
21545
|
+
* Whether a value is usable as the canonical `headers` map, whose schema is
|
|
21546
|
+
* `record(string, string)`. Checked in both directions so a hand-written
|
|
21547
|
+
* `http_headers` can never be imported into a `.rulesync/mcp.jsonc` that the
|
|
21548
|
+
* next generate would refuse to parse.
|
|
21549
|
+
*/
|
|
21550
|
+
function isHeadersRecord(value) {
|
|
21551
|
+
return isPlainObject$1(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
21552
|
+
}
|
|
21553
|
+
/**
|
|
21554
|
+
* Whether a server config describes a Codex stdio server. Codex branches on
|
|
21555
|
+
* `command` first; a config carrying both `command` and `url` is classified as
|
|
21556
|
+
* stdio here, which matches upstream in the sense that it never reaches the
|
|
21557
|
+
* remote arm — upstream rejects that combination outright ("url is not
|
|
21558
|
+
* supported for stdio").
|
|
21559
|
+
*/
|
|
21560
|
+
function isCodexStdioServer(config) {
|
|
21561
|
+
return config["command"] !== void 0;
|
|
21562
|
+
}
|
|
21563
|
+
/**
|
|
20766
21564
|
* `env_vars` entries are either a bare variable name or `{ name, source }`,
|
|
20767
21565
|
* where `source = "remote"` reads the variable from the remote executor
|
|
20768
21566
|
* environment. The other renamed keys (`enabled_tools`, `disabled_tools`) stay
|
|
@@ -20820,6 +21618,75 @@ function normalizeCodexMcpServerName(name) {
|
|
|
20820
21618
|
usedFallback: true
|
|
20821
21619
|
};
|
|
20822
21620
|
}
|
|
21621
|
+
/**
|
|
21622
|
+
* Translate the Codex-native per-server keys that carry a canonical
|
|
21623
|
+
* counterpart under a different name or unit. Returns `undefined` for a key
|
|
21624
|
+
* this translation does not own, leaving it to the caller's other branches.
|
|
21625
|
+
*/
|
|
21626
|
+
function translateCodexOnlyKey({ key, value, config, serverName }) {
|
|
21627
|
+
if (key === "tools") return {};
|
|
21628
|
+
if (key === "http_headers") {
|
|
21629
|
+
if ("headers" in config) return {};
|
|
21630
|
+
if (isHeadersRecord(value)) return { entry: ["headers", omitPrototypePollutionKeys(value)] };
|
|
21631
|
+
warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${serverName}: expected a table of string values`);
|
|
21632
|
+
return {};
|
|
21633
|
+
}
|
|
21634
|
+
const mappedKey = CODEX_TO_RULESYNC_TIMEOUT_FIELD_MAP[key];
|
|
21635
|
+
if (mappedKey) {
|
|
21636
|
+
if (isTimeoutValue(value)) return { entry: [mappedKey, value * MILLISECONDS_PER_SECOND] };
|
|
21637
|
+
warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${serverName}: expected a non-negative number of seconds`);
|
|
21638
|
+
return {};
|
|
21639
|
+
}
|
|
21640
|
+
if (key === "startup_timeout_ms") {
|
|
21641
|
+
if ("startup_timeout_sec" in config) return {};
|
|
21642
|
+
if (isTimeoutValue(value)) return { entry: ["networkTimeout", value] };
|
|
21643
|
+
warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${serverName}: expected a non-negative number of milliseconds`);
|
|
21644
|
+
return {};
|
|
21645
|
+
}
|
|
21646
|
+
}
|
|
21647
|
+
/**
|
|
21648
|
+
* Translate the canonical per-server keys that Codex spells differently, reads
|
|
21649
|
+
* in another unit, or cannot accept at all. Returns `undefined` for a key this
|
|
21650
|
+
* translation does not own.
|
|
21651
|
+
*/
|
|
21652
|
+
function translateCanonicalKeyToCodex({ key, value, isStdio, serverName }) {
|
|
21653
|
+
if (key === "tools") {
|
|
21654
|
+
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.`);
|
|
21655
|
+
return {};
|
|
21656
|
+
}
|
|
21657
|
+
if (CODEX_UNSUPPORTED_CANONICAL_KEYS.has(key)) return {};
|
|
21658
|
+
if (key === "headers") {
|
|
21659
|
+
if (!isHeadersRecord(value)) {
|
|
21660
|
+
warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key 'headers': expected a table of string values, got ${typeof value}`);
|
|
21661
|
+
return {};
|
|
21662
|
+
}
|
|
21663
|
+
if (isStdio) {
|
|
21664
|
+
warnWithFallback(void 0, `[CodexCliMcp] Dropping 'headers' from stdio MCP server "${serverName}": Codex accepts HTTP headers only on url-based servers.`);
|
|
21665
|
+
return {};
|
|
21666
|
+
}
|
|
21667
|
+
return { entry: ["http_headers", omitPrototypePollutionKeys(value)] };
|
|
21668
|
+
}
|
|
21669
|
+
const mappedKey = RULESYNC_TO_CODEX_TIMEOUT_FIELD_MAP[key];
|
|
21670
|
+
if (mappedKey) {
|
|
21671
|
+
if (isTimeoutValue(value)) return { entry: [mappedKey, value / MILLISECONDS_PER_SECOND] };
|
|
21672
|
+
warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected a non-negative number of milliseconds, got ${typeof value}`);
|
|
21673
|
+
return {};
|
|
21674
|
+
}
|
|
21675
|
+
}
|
|
21676
|
+
/**
|
|
21677
|
+
* Codex states no transport of its own — it infers one from `command` versus
|
|
21678
|
+
* `url`, which is why generate drops the canonical `type`. Restate it for a url
|
|
21679
|
+
* server on the way back, so a config imported from Codex reaches the adapters
|
|
21680
|
+
* that branch on `type` as a remote server rather than one with no transport at
|
|
21681
|
+
* all. `streamable_http` is Codex's only remote transport, and canonical spells
|
|
21682
|
+
* that `http`.
|
|
21683
|
+
*/
|
|
21684
|
+
function restateCanonicalTransport(converted) {
|
|
21685
|
+
if (converted["type"] !== void 0) return;
|
|
21686
|
+
if (isCodexStdioServer(converted)) return;
|
|
21687
|
+
if (typeof converted["url"] !== "string") return;
|
|
21688
|
+
converted["type"] = "http";
|
|
21689
|
+
}
|
|
20823
21690
|
function convertFromCodexFormat(codexMcp) {
|
|
20824
21691
|
const result = {};
|
|
20825
21692
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
@@ -20827,7 +21694,15 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
20827
21694
|
const converted = {};
|
|
20828
21695
|
for (const [key, value] of Object.entries(config)) {
|
|
20829
21696
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20830
|
-
|
|
21697
|
+
const codexOnly = translateCodexOnlyKey({
|
|
21698
|
+
key,
|
|
21699
|
+
value,
|
|
21700
|
+
config,
|
|
21701
|
+
serverName: name
|
|
21702
|
+
});
|
|
21703
|
+
if (codexOnly) {
|
|
21704
|
+
if (codexOnly.entry) converted[codexOnly.entry[0]] = codexOnly.entry[1];
|
|
21705
|
+
} else if (key === "enabled") {
|
|
20831
21706
|
if (value === false) converted["disabled"] = true;
|
|
20832
21707
|
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
|
|
20833
21708
|
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
@@ -20840,6 +21715,7 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
20840
21715
|
else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
|
|
20841
21716
|
} else converted[key] = value;
|
|
20842
21717
|
}
|
|
21718
|
+
restateCanonicalTransport(converted);
|
|
20843
21719
|
result[name] = converted;
|
|
20844
21720
|
}
|
|
20845
21721
|
return result;
|
|
@@ -20852,9 +21728,18 @@ function convertToCodexFormat(mcpServers) {
|
|
|
20852
21728
|
const { codexName, usedFallback } = normalizeCodexMcpServerName(name);
|
|
20853
21729
|
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.`);
|
|
20854
21730
|
const converted = {};
|
|
21731
|
+
const isStdio = isCodexStdioServer(config);
|
|
20855
21732
|
for (const [key, value] of Object.entries(config)) {
|
|
20856
21733
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
20857
|
-
|
|
21734
|
+
const translated = translateCanonicalKeyToCodex({
|
|
21735
|
+
key,
|
|
21736
|
+
value,
|
|
21737
|
+
isStdio,
|
|
21738
|
+
serverName: name
|
|
21739
|
+
});
|
|
21740
|
+
if (translated) {
|
|
21741
|
+
if (translated.entry) converted[translated.entry[0]] = translated.entry[1];
|
|
21742
|
+
} else if (key === "disabled") {
|
|
20858
21743
|
if (value === true) converted["enabled"] = false;
|
|
20859
21744
|
} else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
|
|
20860
21745
|
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
@@ -22297,7 +23182,8 @@ function resolveHermesTimeout(config) {
|
|
|
22297
23182
|
* Copies the advanced Hermes-recognized per-server fields that have no canonical
|
|
22298
23183
|
* alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
|
|
22299
23184
|
* path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
|
|
22300
|
-
* `connect_timeout` (seconds),
|
|
23185
|
+
* `connect_timeout` (seconds), `supports_parallel_tool_calls`,
|
|
23186
|
+
* `keepalive_interval`, and `elicitation` — verbatim
|
|
22301
23187
|
* from `source` to `target`. Field names are identical on both sides (the
|
|
22302
23188
|
* canonical `McpServerSchema` is a `looseObject`), so this serves export and
|
|
22303
23189
|
* import alike. See the Hermes mcp-config-reference.
|
|
@@ -22358,6 +23244,14 @@ function copyHermesAdvancedFields(source, target) {
|
|
|
22358
23244
|
target.sampling = omitPrototypePollutionKeys(structuredClone(source.sampling));
|
|
22359
23245
|
copied = true;
|
|
22360
23246
|
}
|
|
23247
|
+
if (typeof source.keepalive_interval === "number") {
|
|
23248
|
+
target.keepalive_interval = source.keepalive_interval;
|
|
23249
|
+
copied = true;
|
|
23250
|
+
}
|
|
23251
|
+
if (isPlainObject$1(source.elicitation)) {
|
|
23252
|
+
target.elicitation = omitPrototypePollutionKeys(structuredClone(source.elicitation));
|
|
23253
|
+
copied = true;
|
|
23254
|
+
}
|
|
22361
23255
|
return copied;
|
|
22362
23256
|
}
|
|
22363
23257
|
/**
|
|
@@ -22401,8 +23295,10 @@ function applyHermesToolsBlock(hermesTools, server) {
|
|
|
22401
23295
|
* `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
|
|
22402
23296
|
* exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
|
|
22403
23297
|
* fields Hermes understands are emitted, so the shared `config.yaml` is not
|
|
22404
|
-
* polluted with canonical-only aliases (`type`, `
|
|
22405
|
-
*
|
|
23298
|
+
* polluted with canonical-only aliases (`type`, `httpUrl`, `networkTimeout`,
|
|
23299
|
+
* ...) — with one exception since v0.20.0: a canonical `sse` server is written
|
|
23300
|
+
* as Hermes's own `transport: sse`, without which Hermes would connect to it
|
|
23301
|
+
* over Streamable HTTP.
|
|
22406
23302
|
*/
|
|
22407
23303
|
function convertServerToHermes(config) {
|
|
22408
23304
|
const out = {};
|
|
@@ -22422,6 +23318,7 @@ function convertServerToHermes(config) {
|
|
|
22422
23318
|
} else if (url !== void 0) {
|
|
22423
23319
|
out.url = url;
|
|
22424
23320
|
if (isPlainObject$1(config.headers)) out.headers = omitPrototypePollutionKeys(config.headers);
|
|
23321
|
+
if (config.type === "sse" || config.transport === "sse") out.transport = "sse";
|
|
22425
23322
|
}
|
|
22426
23323
|
if (config.disabled === true) out.enabled = false;
|
|
22427
23324
|
const timeout = resolveHermesTimeout(config);
|
|
@@ -22469,6 +23366,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
22469
23366
|
if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
|
|
22470
23367
|
if (typeof config.url === "string") server.url = config.url;
|
|
22471
23368
|
if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
23369
|
+
if (typeof config.url === "string" && config.transport === "sse") server.type = "sse";
|
|
22472
23370
|
if (config.enabled === false) server.disabled = true;
|
|
22473
23371
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
22474
23372
|
if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
@@ -24041,6 +24939,7 @@ const REASONIX_PLUGIN_FIELDS = [
|
|
|
24041
24939
|
"env",
|
|
24042
24940
|
"url",
|
|
24043
24941
|
"headers",
|
|
24942
|
+
"startup_timeout_seconds",
|
|
24044
24943
|
"call_timeout_seconds",
|
|
24045
24944
|
"tool_timeout_seconds"
|
|
24046
24945
|
];
|
|
@@ -24717,20 +25616,6 @@ function deriveTransportAllowlist(servers) {
|
|
|
24717
25616
|
return allowlist;
|
|
24718
25617
|
}
|
|
24719
25618
|
//#endregion
|
|
24720
|
-
//#region src/features/shared/vibe-config-scope.ts
|
|
24721
|
-
/**
|
|
24722
|
-
* Vibe selects exactly **one** persistence TOML and does not merge scopes: the
|
|
24723
|
-
* trusted project `.vibe/config.toml` when one is discovered, otherwise
|
|
24724
|
-
* `~/.vibe/config.toml` (single code path since v2.22.0's ConfigOrchestrator
|
|
24725
|
-
* migration). A `--global` run that writes the home file is therefore inert in
|
|
24726
|
-
* any project that has its own config — worth a heads-up, since the other Vibe
|
|
24727
|
-
* surfaces (rules, hooks, agents, skills) genuinely combine scopes.
|
|
24728
|
-
*/
|
|
24729
|
-
async function warnIfGlobalVibeConfigIsShadowed(logger) {
|
|
24730
|
-
if (!await fileExists(join(process.cwd(), ".vibe", "config.toml"))) return;
|
|
24731
|
-
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.");
|
|
24732
|
-
}
|
|
24733
|
-
//#endregion
|
|
24734
25619
|
//#region src/features/mcp/vibe-mcp.ts
|
|
24735
25620
|
const VIBE_MCP_SERVER_FIELDS = [
|
|
24736
25621
|
"transport",
|
|
@@ -24799,9 +25684,8 @@ var VibeMcp = class VibeMcp extends ToolMcp {
|
|
|
24799
25684
|
global
|
|
24800
25685
|
});
|
|
24801
25686
|
}
|
|
24802
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true,
|
|
25687
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
24803
25688
|
const paths = this.getSettablePaths({ global });
|
|
24804
|
-
if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
|
|
24805
25689
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
24806
25690
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
24807
25691
|
const existingServers = normalizeMcpServersArray(parseSharedConfig({
|
|
@@ -28456,6 +29340,81 @@ function asCursorPermissionEntryArray(value, logger, fieldLabel) {
|
|
|
28456
29340
|
else logger?.warn(`Cursor CLI permissions${fieldLabel ? `.${fieldLabel}` : ""} contains a non-string entry; dropping ${JSON.stringify(item)}.`);
|
|
28457
29341
|
return result;
|
|
28458
29342
|
}
|
|
29343
|
+
/**
|
|
29344
|
+
* Assemble the Cursor CLI config to write.
|
|
29345
|
+
*
|
|
29346
|
+
* Cursor scopes the file asymmetrically: "Only permissions can be configured at
|
|
29347
|
+
* the project level. All other CLI settings must be set globally." So
|
|
29348
|
+
* `version`, `editor.vimMode`, `approvalMode` and `sandbox` belong to
|
|
29349
|
+
* `~/.cursor/cli-config.json` alone — writing them into `.cursor/cli.json`
|
|
29350
|
+
* produces keys Cursor ignores, which silently strands an authored
|
|
29351
|
+
* `cursor.approvalMode`.
|
|
29352
|
+
*
|
|
29353
|
+
* @see https://cursor.com/docs/cli/reference/configuration
|
|
29354
|
+
*/
|
|
29355
|
+
function mergeCursorCliConfig({ settings, mergedPermissions, cursorOverride, global, filePath, logger }) {
|
|
29356
|
+
if (!global) {
|
|
29357
|
+
warnAboutGlobalOnlyOverrideKeys({
|
|
29358
|
+
cursorOverride,
|
|
29359
|
+
filePath,
|
|
29360
|
+
logger
|
|
29361
|
+
});
|
|
29362
|
+
return {
|
|
29363
|
+
...settings,
|
|
29364
|
+
permissions: mergedPermissions
|
|
29365
|
+
};
|
|
29366
|
+
}
|
|
29367
|
+
const existingEditor = asExistingEditor({
|
|
29368
|
+
settings,
|
|
29369
|
+
filePath,
|
|
29370
|
+
logger
|
|
29371
|
+
});
|
|
29372
|
+
return {
|
|
29373
|
+
...settings,
|
|
29374
|
+
...cursorOverride,
|
|
29375
|
+
version: settings.version ?? 1,
|
|
29376
|
+
editor: {
|
|
29377
|
+
...existingEditor,
|
|
29378
|
+
vimMode: existingEditor.vimMode ?? false
|
|
29379
|
+
},
|
|
29380
|
+
permissions: mergedPermissions
|
|
29381
|
+
};
|
|
29382
|
+
}
|
|
29383
|
+
/**
|
|
29384
|
+
* The existing `editor` object to merge rulesync's managed `vimMode` into.
|
|
29385
|
+
* Only called in global scope: a project config's `editor` is passed through
|
|
29386
|
+
* untouched, so narrowing it there would warn about a value nothing ignores.
|
|
29387
|
+
*/
|
|
29388
|
+
function asExistingEditor({ settings, filePath, logger }) {
|
|
29389
|
+
const raw = settings.editor;
|
|
29390
|
+
if (raw === void 0) return {};
|
|
29391
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
29392
|
+
logger?.warn(`Cursor CLI config at ${filePath} has a non-object \`editor\` field; ignoring existing editor settings.`);
|
|
29393
|
+
return {};
|
|
29394
|
+
}
|
|
29395
|
+
/**
|
|
29396
|
+
* Override keys that never reach the file from the override in any scope,
|
|
29397
|
+
* because rulesync re-applies its own managed value over them. Naming them in
|
|
29398
|
+
* the project-scope warning would wrongly promise that `--global` makes them
|
|
29399
|
+
* take effect.
|
|
29400
|
+
*/
|
|
29401
|
+
const CURSOR_OVERRIDE_KEYS_ALWAYS_CLOBBERED = /* @__PURE__ */ new Set([
|
|
29402
|
+
"version",
|
|
29403
|
+
"editor",
|
|
29404
|
+
"permissions"
|
|
29405
|
+
]);
|
|
29406
|
+
/**
|
|
29407
|
+
* Name the override keys that would have been written in global scope but are
|
|
29408
|
+
* dropped here, so the user learns the setting did not take effect and how to
|
|
29409
|
+
* make it.
|
|
29410
|
+
*/
|
|
29411
|
+
function warnAboutGlobalOnlyOverrideKeys({ cursorOverride, filePath, logger }) {
|
|
29412
|
+
const strandedKeys = Object.keys(cursorOverride).filter((key) => !CURSOR_OVERRIDE_KEYS_ALWAYS_CLOBBERED.has(key)).toSorted();
|
|
29413
|
+
if (strandedKeys.length === 0) return;
|
|
29414
|
+
const names = strandedKeys.map((key) => `\`${key}\``).join(", ");
|
|
29415
|
+
const [wasWere, itThem] = strandedKeys.length === 1 ? ["was", "it"] : ["were", "them"];
|
|
29416
|
+
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.`);
|
|
29417
|
+
}
|
|
28459
29418
|
var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
28460
29419
|
constructor(params) {
|
|
28461
29420
|
super({
|
|
@@ -28496,14 +29455,6 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
28496
29455
|
const config = rulesyncPermissions.getJson();
|
|
28497
29456
|
const { allow, deny } = convertRulesyncToCursorPermissions(config, logger);
|
|
28498
29457
|
const managedTypes = new Set(Object.keys(config.permission).map((category) => toCursorType(category)));
|
|
28499
|
-
const existingEditorRaw = settings.editor;
|
|
28500
|
-
let existingEditor;
|
|
28501
|
-
if (existingEditorRaw === void 0) existingEditor = {};
|
|
28502
|
-
else if (existingEditorRaw !== null && typeof existingEditorRaw === "object" && !Array.isArray(existingEditorRaw)) existingEditor = existingEditorRaw;
|
|
28503
|
-
else {
|
|
28504
|
-
logger?.warn(`Cursor CLI config at ${filePath} has a non-object \`editor\` field; ignoring existing editor settings.`);
|
|
28505
|
-
existingEditor = {};
|
|
28506
|
-
}
|
|
28507
29458
|
const existingPermissionsRaw = settings.permissions;
|
|
28508
29459
|
let existingPermissions;
|
|
28509
29460
|
if (existingPermissionsRaw === void 0) existingPermissions = {};
|
|
@@ -28520,17 +29471,14 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
28520
29471
|
mergedPermissions.allow = mergedAllow;
|
|
28521
29472
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
28522
29473
|
else delete mergedPermissions.deny;
|
|
28523
|
-
const
|
|
28524
|
-
|
|
28525
|
-
|
|
28526
|
-
|
|
28527
|
-
|
|
28528
|
-
|
|
28529
|
-
|
|
28530
|
-
|
|
28531
|
-
},
|
|
28532
|
-
permissions: mergedPermissions
|
|
28533
|
-
};
|
|
29474
|
+
const merged = mergeCursorCliConfig({
|
|
29475
|
+
settings,
|
|
29476
|
+
mergedPermissions,
|
|
29477
|
+
cursorOverride: config.cursor ?? {},
|
|
29478
|
+
global,
|
|
29479
|
+
filePath,
|
|
29480
|
+
logger
|
|
29481
|
+
});
|
|
28534
29482
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
28535
29483
|
return new CursorPermissions({
|
|
28536
29484
|
outputRoot,
|
|
@@ -29244,14 +30192,16 @@ const CATEGORY_TO_GROK_TOOL = {
|
|
|
29244
30192
|
edit: "Edit",
|
|
29245
30193
|
write: "Edit",
|
|
29246
30194
|
grep: "Grep",
|
|
29247
|
-
webfetch: "WebFetch"
|
|
30195
|
+
webfetch: "WebFetch",
|
|
30196
|
+
websearch: "WebSearch"
|
|
29248
30197
|
};
|
|
29249
30198
|
const GROK_TOOL_TO_CATEGORY = {
|
|
29250
30199
|
Bash: "bash",
|
|
29251
30200
|
Read: "read",
|
|
29252
30201
|
Edit: "edit",
|
|
29253
30202
|
Grep: "grep",
|
|
29254
|
-
WebFetch: "webfetch"
|
|
30203
|
+
WebFetch: "webfetch",
|
|
30204
|
+
WebSearch: "websearch"
|
|
29255
30205
|
};
|
|
29256
30206
|
const GROK_MCP_TOOL = "MCPTool";
|
|
29257
30207
|
/**
|
|
@@ -29310,18 +30260,18 @@ function parseGrokEntry(entry) {
|
|
|
29310
30260
|
*
|
|
29311
30261
|
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
29312
30262
|
* `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
|
|
29313
|
-
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`,
|
|
29314
|
-
* `WebFetch`, evaluated with precedence `deny > ask > allow`
|
|
30263
|
+
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`,
|
|
30264
|
+
* `WebFetch`, and `WebSearch`, evaluated with precedence `deny > ask > allow`
|
|
29315
30265
|
* (https://docs.x.ai/build/settings/reference). rulesync's canonical
|
|
29316
30266
|
* per-category, per-pattern model maps almost 1:1:
|
|
29317
30267
|
* - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
|
|
29318
30268
|
* the matching Grok entry and is bucketed into the `[permission]` array for
|
|
29319
|
-
* that action. `bash|read|edit|grep|webfetch` map to their Grok
|
|
29320
|
-
* `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*`
|
|
29321
|
-
* `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
30269
|
+
* that action. `bash|read|edit|grep|webfetch|websearch` map to their Grok
|
|
30270
|
+
* tool; `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*`
|
|
30271
|
+
* maps to `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
29322
30272
|
* parentheses, so a non-`*` argument pattern on it is not represented).
|
|
29323
|
-
* Categories with no Grok tool (`
|
|
29324
|
-
*
|
|
30273
|
+
* Categories with no Grok tool (`glob`, `notebookedit`, `agent`) are
|
|
30274
|
+
* skipped (with a warning when they carry a `deny` rule, to
|
|
29325
30275
|
* surface the gap). When two canonical rules collapse onto the same Grok
|
|
29326
30276
|
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
29327
30277
|
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
@@ -29348,8 +30298,8 @@ function parseGrokEntry(entry) {
|
|
|
29348
30298
|
* tools it models and (in global scope) the `[ui] permission_mode` value, while
|
|
29349
30299
|
* every other key
|
|
29350
30300
|
* (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
|
|
29351
|
-
* entries for
|
|
29352
|
-
* file is never deleted.
|
|
30301
|
+
* entries for tool prefixes rulesync cannot model (e.g. `any`) are preserved.
|
|
30302
|
+
* The file is never deleted.
|
|
29353
30303
|
*/
|
|
29354
30304
|
var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
29355
30305
|
constructor(params) {
|
|
@@ -29456,7 +30406,7 @@ const ACTION_RANK = {
|
|
|
29456
30406
|
};
|
|
29457
30407
|
/**
|
|
29458
30408
|
* Collect user-authored entries from an existing `[permission]` array whose
|
|
29459
|
-
* tool prefix rulesync cannot model (e.g. `
|
|
30409
|
+
* tool prefix rulesync cannot model (e.g. `any`). Such entries are
|
|
29460
30410
|
* preserved verbatim so replacing the arrays does not silently drop them
|
|
29461
30411
|
* (mirrors the Cursor adapter's preservation of unmanaged types).
|
|
29462
30412
|
*/
|
|
@@ -31192,7 +32142,12 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
31192
32142
|
"disabled",
|
|
31193
32143
|
"visible"
|
|
31194
32144
|
];
|
|
31195
|
-
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
32145
|
+
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
32146
|
+
"folderTrust",
|
|
32147
|
+
"allowedHttpHookUrls",
|
|
32148
|
+
"allowPrivateNetworkHooks"
|
|
32149
|
+
];
|
|
32150
|
+
const QWEN_GLOBAL_ONLY_SECURITY_KEYS = ["allowPrivateNetworkHooks"];
|
|
31196
32151
|
const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
|
|
31197
32152
|
function asPlainRecord(value) {
|
|
31198
32153
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -31204,6 +32159,21 @@ function pickQwenOverrideKeys(group, keys) {
|
|
|
31204
32159
|
for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
|
|
31205
32160
|
return picked;
|
|
31206
32161
|
}
|
|
32162
|
+
/**
|
|
32163
|
+
* Drop the global-only `security` keys from the override when generating project
|
|
32164
|
+
* settings, warning once per dropped key. Only the override copy is filtered, so
|
|
32165
|
+
* a value the user already wrote into the project file stays untouched.
|
|
32166
|
+
*/
|
|
32167
|
+
function scopeOverrideSecurity(overrideSecurity, { global, relativeFilePath, logger }) {
|
|
32168
|
+
const scoped = { ...asPlainRecord(overrideSecurity) };
|
|
32169
|
+
if (global) return scoped;
|
|
32170
|
+
for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
|
|
32171
|
+
if (scoped[key] === void 0) continue;
|
|
32172
|
+
delete scoped[key];
|
|
32173
|
+
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.`);
|
|
32174
|
+
}
|
|
32175
|
+
return scoped;
|
|
32176
|
+
}
|
|
31207
32177
|
var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
31208
32178
|
constructor(params) {
|
|
31209
32179
|
super({
|
|
@@ -31272,10 +32242,18 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
31272
32242
|
...asPlainRecord(settings.tools),
|
|
31273
32243
|
...asPlainRecord(override.tools)
|
|
31274
32244
|
};
|
|
31275
|
-
if (override?.security !== void 0)
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
32245
|
+
if (override?.security !== void 0) {
|
|
32246
|
+
const scopedSecurity = scopeOverrideSecurity(override.security, {
|
|
32247
|
+
global,
|
|
32248
|
+
relativeFilePath: paths.relativeFilePath,
|
|
32249
|
+
logger
|
|
32250
|
+
});
|
|
32251
|
+
const mergedSecurity = {
|
|
32252
|
+
...asPlainRecord(settings.security),
|
|
32253
|
+
...scopedSecurity
|
|
32254
|
+
};
|
|
32255
|
+
if (Object.keys(mergedSecurity).length > 0) patch.security = mergedSecurity;
|
|
32256
|
+
}
|
|
31279
32257
|
const fileContent = applySharedConfigPatch({
|
|
31280
32258
|
fileKey: sharedConfigFileKey(paths),
|
|
31281
32259
|
feature: "permissions",
|
|
@@ -31309,6 +32287,10 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
31309
32287
|
});
|
|
31310
32288
|
const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
|
|
31311
32289
|
const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
|
|
32290
|
+
for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
|
|
32291
|
+
if (overrideSecurity[key] === void 0) continue;
|
|
32292
|
+
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.`);
|
|
32293
|
+
}
|
|
31312
32294
|
const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
|
|
31313
32295
|
const qwencodeOverride = {};
|
|
31314
32296
|
if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
|
|
@@ -32484,7 +33466,6 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
32484
33466
|
}
|
|
32485
33467
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
32486
33468
|
const paths = this.getSettablePaths({ global });
|
|
32487
|
-
if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
|
|
32488
33469
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
32489
33470
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
32490
33471
|
const config = parseVibeConfig(existingContent);
|
|
@@ -32766,7 +33747,9 @@ function warpSettingsDir() {
|
|
|
32766
33747
|
* `[agents.execution_profiles.<id>]` collection:
|
|
32767
33748
|
* - `command_allowlist` — commands that auto-execute.
|
|
32768
33749
|
* - `command_denylist` — commands that always require permission (the denylist
|
|
32769
|
-
* wins over the allowlist).
|
|
33750
|
+
* wins over the allowlist). Writing it at all replaces Warp's built-in
|
|
33751
|
+
* default denylist, so rulesync warns whenever it emits a non-empty one.
|
|
33752
|
+
* https://docs.warp.dev/cli/permissions-and-profiles/
|
|
32770
33753
|
*
|
|
32771
33754
|
* The legacy `[agents.profiles]` keys
|
|
32772
33755
|
* (`agent_mode_command_execution_allowlist` / `denylist`) are consumed only
|
|
@@ -32864,6 +33847,7 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
32864
33847
|
if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
|
|
32865
33848
|
else delete profiles[DENYLIST_KEY];
|
|
32866
33849
|
agents.profiles = profiles;
|
|
33850
|
+
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.`);
|
|
32867
33851
|
mergeIntoDefaultExecutionProfile({
|
|
32868
33852
|
agents,
|
|
32869
33853
|
mergedAllow,
|
|
@@ -34795,7 +35779,10 @@ const ClaudecodeSkillFrontmatterSchema = z.looseObject({
|
|
|
34795
35779
|
shell: z.optional(z.string()),
|
|
34796
35780
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
34797
35781
|
"user-invocable": z.optional(z.boolean()),
|
|
34798
|
-
paths: z.optional(z.union([z.string(), z.array(z.string())]))
|
|
35782
|
+
paths: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
35783
|
+
license: z.optional(z.string()),
|
|
35784
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
35785
|
+
metadata: z.optional(z.looseObject({}))
|
|
34799
35786
|
});
|
|
34800
35787
|
/**
|
|
34801
35788
|
* Builds the Claude Code SKILL.md frontmatter from a rulesync skill, carrying
|
|
@@ -34822,7 +35809,10 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
34822
35809
|
hooks: section.hooks,
|
|
34823
35810
|
"disable-model-invocation": resolvedDisableModelInvocation,
|
|
34824
35811
|
"user-invocable": resolvedUserInvocable,
|
|
34825
|
-
paths: section.paths
|
|
35812
|
+
paths: section.paths,
|
|
35813
|
+
license: section.license,
|
|
35814
|
+
compatibility: section.compatibility,
|
|
35815
|
+
metadata: section.metadata
|
|
34826
35816
|
};
|
|
34827
35817
|
const frontmatter = {
|
|
34828
35818
|
name: rulesyncFrontmatter.name,
|
|
@@ -34833,6 +35823,115 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
34833
35823
|
return frontmatter;
|
|
34834
35824
|
}
|
|
34835
35825
|
/**
|
|
35826
|
+
* Builds the `claudecode:` section of a rulesync skill from a Claude Code
|
|
35827
|
+
* SKILL.md frontmatter — the inverse of `buildClaudecodeSkillFrontmatter`, and
|
|
35828
|
+
* extracted for the same reason: to keep `toRulesyncSkill` under the
|
|
35829
|
+
* cyclomatic-complexity cap as fields are added. The truthy/defined split
|
|
35830
|
+
* mirrors that function exactly so the conversion stays symmetric.
|
|
35831
|
+
*/
|
|
35832
|
+
function buildClaudecodeSkillSection({ frontmatter, resolvedPaths, scheduledTask }) {
|
|
35833
|
+
const fields = [
|
|
35834
|
+
[
|
|
35835
|
+
"when_to_use",
|
|
35836
|
+
frontmatter.when_to_use,
|
|
35837
|
+
"truthy"
|
|
35838
|
+
],
|
|
35839
|
+
[
|
|
35840
|
+
"allowed-tools",
|
|
35841
|
+
frontmatter["allowed-tools"],
|
|
35842
|
+
"truthy"
|
|
35843
|
+
],
|
|
35844
|
+
[
|
|
35845
|
+
"disallowed-tools",
|
|
35846
|
+
frontmatter["disallowed-tools"],
|
|
35847
|
+
"truthy"
|
|
35848
|
+
],
|
|
35849
|
+
[
|
|
35850
|
+
"model",
|
|
35851
|
+
frontmatter.model,
|
|
35852
|
+
"truthy"
|
|
35853
|
+
],
|
|
35854
|
+
[
|
|
35855
|
+
"effort",
|
|
35856
|
+
frontmatter.effort,
|
|
35857
|
+
"truthy"
|
|
35858
|
+
],
|
|
35859
|
+
[
|
|
35860
|
+
"argument-hint",
|
|
35861
|
+
frontmatter["argument-hint"],
|
|
35862
|
+
"truthy"
|
|
35863
|
+
],
|
|
35864
|
+
[
|
|
35865
|
+
"arguments",
|
|
35866
|
+
frontmatter.arguments,
|
|
35867
|
+
"defined"
|
|
35868
|
+
],
|
|
35869
|
+
[
|
|
35870
|
+
"context",
|
|
35871
|
+
frontmatter.context,
|
|
35872
|
+
"truthy"
|
|
35873
|
+
],
|
|
35874
|
+
[
|
|
35875
|
+
"agent",
|
|
35876
|
+
frontmatter.agent,
|
|
35877
|
+
"truthy"
|
|
35878
|
+
],
|
|
35879
|
+
[
|
|
35880
|
+
"background",
|
|
35881
|
+
frontmatter.background,
|
|
35882
|
+
"defined"
|
|
35883
|
+
],
|
|
35884
|
+
[
|
|
35885
|
+
"hooks",
|
|
35886
|
+
frontmatter.hooks,
|
|
35887
|
+
"defined"
|
|
35888
|
+
],
|
|
35889
|
+
[
|
|
35890
|
+
"shell",
|
|
35891
|
+
frontmatter.shell,
|
|
35892
|
+
"truthy"
|
|
35893
|
+
],
|
|
35894
|
+
[
|
|
35895
|
+
"disable-model-invocation",
|
|
35896
|
+
frontmatter["disable-model-invocation"],
|
|
35897
|
+
"defined"
|
|
35898
|
+
],
|
|
35899
|
+
[
|
|
35900
|
+
"user-invocable",
|
|
35901
|
+
frontmatter["user-invocable"],
|
|
35902
|
+
"defined"
|
|
35903
|
+
],
|
|
35904
|
+
[
|
|
35905
|
+
"scheduled-task",
|
|
35906
|
+
scheduledTask || void 0,
|
|
35907
|
+
"defined"
|
|
35908
|
+
],
|
|
35909
|
+
[
|
|
35910
|
+
"paths",
|
|
35911
|
+
resolvedPaths,
|
|
35912
|
+
"defined"
|
|
35913
|
+
],
|
|
35914
|
+
[
|
|
35915
|
+
"license",
|
|
35916
|
+
frontmatter.license,
|
|
35917
|
+
"defined"
|
|
35918
|
+
],
|
|
35919
|
+
[
|
|
35920
|
+
"compatibility",
|
|
35921
|
+
frontmatter.compatibility,
|
|
35922
|
+
"defined"
|
|
35923
|
+
],
|
|
35924
|
+
[
|
|
35925
|
+
"metadata",
|
|
35926
|
+
frontmatter.metadata,
|
|
35927
|
+
"defined"
|
|
35928
|
+
]
|
|
35929
|
+
];
|
|
35930
|
+
const section = {};
|
|
35931
|
+
for (const [key, value, presence] of fields) if (presence === "truthy" ? Boolean(value) : value !== void 0) section[key] = value;
|
|
35932
|
+
return section;
|
|
35933
|
+
}
|
|
35934
|
+
/**
|
|
34836
35935
|
* Escapes the glob metacharacters in a directory path so it matches literally.
|
|
34837
35936
|
* A real directory name may contain them — `app/[slug]` in a Next.js tree is
|
|
34838
35937
|
* the common case, and unescaped `[slug]` reads as a bracket expression that
|
|
@@ -34953,25 +36052,11 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
34953
36052
|
}
|
|
34954
36053
|
toRulesyncSkill() {
|
|
34955
36054
|
const frontmatter = this.getFrontmatter();
|
|
34956
|
-
const
|
|
34957
|
-
|
|
34958
|
-
|
|
34959
|
-
|
|
34960
|
-
|
|
34961
|
-
...frontmatter.model && { model: frontmatter.model },
|
|
34962
|
-
...frontmatter.effort && { effort: frontmatter.effort },
|
|
34963
|
-
...frontmatter["argument-hint"] && { "argument-hint": frontmatter["argument-hint"] },
|
|
34964
|
-
...frontmatter.arguments !== void 0 && { arguments: frontmatter.arguments },
|
|
34965
|
-
...frontmatter.context && { context: frontmatter.context },
|
|
34966
|
-
...frontmatter.agent && { agent: frontmatter.agent },
|
|
34967
|
-
...frontmatter.background !== void 0 && { background: frontmatter.background },
|
|
34968
|
-
...frontmatter.hooks !== void 0 && { hooks: frontmatter.hooks },
|
|
34969
|
-
...frontmatter.shell && { shell: frontmatter.shell },
|
|
34970
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
34971
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
34972
|
-
...this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true },
|
|
34973
|
-
...resolvedPaths !== void 0 && { paths: resolvedPaths }
|
|
34974
|
-
};
|
|
36055
|
+
const claudecodeSection = buildClaudecodeSkillSection({
|
|
36056
|
+
frontmatter,
|
|
36057
|
+
resolvedPaths: frontmatter.paths !== void 0 ? frontmatter.paths : deriveNestedSkillPaths(this.relativeDirPath),
|
|
36058
|
+
scheduledTask: this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH
|
|
36059
|
+
});
|
|
34975
36060
|
const rulesyncFrontmatter = {
|
|
34976
36061
|
name: frontmatter.name,
|
|
34977
36062
|
description: frontmatter.description,
|
|
@@ -47832,19 +48917,21 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
47832
48917
|
*/
|
|
47833
48918
|
var PiRule = class PiRule extends ToolRule {
|
|
47834
48919
|
appendSystemPrompt;
|
|
47835
|
-
|
|
48920
|
+
contextFileOverride;
|
|
48921
|
+
constructor({ fileContent, root, appendSystemPrompt = false, contextFileOverride = false, ...rest }) {
|
|
47836
48922
|
super({
|
|
47837
48923
|
...rest,
|
|
47838
48924
|
fileContent,
|
|
47839
48925
|
root: root ?? false
|
|
47840
48926
|
});
|
|
47841
48927
|
this.appendSystemPrompt = appendSystemPrompt;
|
|
48928
|
+
this.contextFileOverride = contextFileOverride;
|
|
47842
48929
|
}
|
|
47843
|
-
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
48930
|
+
static getSettablePaths({ global = false, excludeToolDir, contextFile } = {}) {
|
|
47844
48931
|
return {
|
|
47845
48932
|
root: {
|
|
47846
48933
|
relativeDirPath: global ? buildToolPath(".pi", "agent", excludeToolDir) : ".",
|
|
47847
|
-
relativeFilePath: PI_RULE_FILE_NAME
|
|
48934
|
+
relativeFilePath: contextFile === "override" ? PI_RULE_OVERRIDE_FILE_NAME : PI_RULE_FILE_NAME
|
|
47848
48935
|
},
|
|
47849
48936
|
appendSystemPrompt: {
|
|
47850
48937
|
relativeDirPath: global ? buildToolPath(".pi", "agent", excludeToolDir) : ".pi",
|
|
@@ -47858,7 +48945,22 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47858
48945
|
* `APPEND_SYSTEM.md` is cleaned up once no rule opts in anymore.
|
|
47859
48946
|
*/
|
|
47860
48947
|
static getExtraFixedFiles({ global = false } = {}) {
|
|
47861
|
-
return [this.getSettablePaths({ global }).appendSystemPrompt
|
|
48948
|
+
return [this.getSettablePaths({ global }).appendSystemPrompt, this.getSettablePaths({
|
|
48949
|
+
global,
|
|
48950
|
+
contextFile: "override"
|
|
48951
|
+
}).root];
|
|
48952
|
+
}
|
|
48953
|
+
/**
|
|
48954
|
+
* The project-root `AGENTS.md` is written by several other targets
|
|
48955
|
+
* (agentsmd, codexcli, warp, devin, ...), and the root-file ownership map that
|
|
48956
|
+
* arbitrates a shared path only applies to `--check`. With
|
|
48957
|
+
* `pi.contextFile: override` Pi stops writing that file, so leaving it on the
|
|
48958
|
+
* orphan list would make every `pi` generate delete another target's freshly
|
|
48959
|
+
* written output. The global `~/.pi/agent/AGENTS.md` is Pi-exclusive and stays
|
|
48960
|
+
* deletable.
|
|
48961
|
+
*/
|
|
48962
|
+
isDeletable() {
|
|
48963
|
+
return !(this.getRelativeDirPath() === "." && this.getRelativeFilePath() === "AGENTS.md");
|
|
47862
48964
|
}
|
|
47863
48965
|
/**
|
|
47864
48966
|
* Pi appends `APPEND_SYSTEM.md` to the system prompt itself, so listing it in
|
|
@@ -47883,20 +48985,29 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47883
48985
|
appendSystemPrompt: true
|
|
47884
48986
|
});
|
|
47885
48987
|
}
|
|
47886
|
-
const
|
|
48988
|
+
const isOverride = relativeFilePath === PI_RULE_OVERRIDE_FILE_NAME;
|
|
48989
|
+
const rootPaths = isOverride ? this.getSettablePaths({
|
|
48990
|
+
global,
|
|
48991
|
+
contextFile: "override"
|
|
48992
|
+
}).root : root;
|
|
48993
|
+
const relativePath = join(rootPaths.relativeDirPath, rootPaths.relativeFilePath);
|
|
47887
48994
|
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
47888
48995
|
return new PiRule({
|
|
47889
48996
|
outputRoot,
|
|
47890
|
-
relativeDirPath:
|
|
47891
|
-
relativeFilePath:
|
|
48997
|
+
relativeDirPath: rootPaths.relativeDirPath,
|
|
48998
|
+
relativeFilePath: rootPaths.relativeFilePath,
|
|
47892
48999
|
fileContent,
|
|
47893
49000
|
validate,
|
|
47894
|
-
root: true
|
|
49001
|
+
root: true,
|
|
49002
|
+
contextFileOverride: isOverride
|
|
47895
49003
|
});
|
|
47896
49004
|
}
|
|
47897
49005
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
47898
|
-
const { root, appendSystemPrompt } = this.getSettablePaths({ global });
|
|
47899
49006
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
49007
|
+
const { root, appendSystemPrompt } = this.getSettablePaths({
|
|
49008
|
+
global,
|
|
49009
|
+
contextFile: frontmatter.pi?.contextFile
|
|
49010
|
+
});
|
|
47900
49011
|
if (!frontmatter.root && frontmatter.pi?.systemPrompt === "append") return new PiRule({
|
|
47901
49012
|
outputRoot,
|
|
47902
49013
|
relativeDirPath: appendSystemPrompt.relativeDirPath,
|
|
@@ -47913,7 +49024,8 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47913
49024
|
relativeFilePath: root.relativeFilePath,
|
|
47914
49025
|
fileContent: rulesyncRule.getBody(),
|
|
47915
49026
|
validate,
|
|
47916
|
-
root: isRoot
|
|
49027
|
+
root: isRoot,
|
|
49028
|
+
contextFileOverride: frontmatter.pi?.contextFile === "override"
|
|
47917
49029
|
});
|
|
47918
49030
|
}
|
|
47919
49031
|
toRulesyncRule() {
|
|
@@ -47928,6 +49040,18 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47928
49040
|
},
|
|
47929
49041
|
body: this.getFileContent()
|
|
47930
49042
|
});
|
|
49043
|
+
if (this.contextFileOverride) return new RulesyncRule({
|
|
49044
|
+
outputRoot: process.cwd(),
|
|
49045
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
49046
|
+
relativeFilePath: RULESYNC_OVERVIEW_FILE_NAME,
|
|
49047
|
+
frontmatter: {
|
|
49048
|
+
root: true,
|
|
49049
|
+
targets: ["pi"],
|
|
49050
|
+
globs: ["**/*"],
|
|
49051
|
+
pi: { contextFile: "override" }
|
|
49052
|
+
},
|
|
49053
|
+
body: this.getFileContent()
|
|
49054
|
+
});
|
|
47931
49055
|
return this.toRulesyncRuleDefault();
|
|
47932
49056
|
}
|
|
47933
49057
|
validate() {
|
|
@@ -47947,14 +49071,16 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
47947
49071
|
root: false,
|
|
47948
49072
|
appendSystemPrompt: true
|
|
47949
49073
|
});
|
|
47950
|
-
const
|
|
49074
|
+
const isOverride = relativeFilePath === PI_RULE_OVERRIDE_FILE_NAME;
|
|
49075
|
+
const isRoot = (relativeFilePath === "AGENTS.md" || isOverride) && (relativeDirPath === "." || relativeDirPath === root.relativeDirPath);
|
|
47951
49076
|
return new PiRule({
|
|
47952
49077
|
outputRoot,
|
|
47953
49078
|
relativeDirPath,
|
|
47954
49079
|
relativeFilePath,
|
|
47955
49080
|
fileContent: "",
|
|
47956
49081
|
validate: false,
|
|
47957
|
-
root: isRoot
|
|
49082
|
+
root: isRoot,
|
|
49083
|
+
contextFileOverride: isOverride
|
|
47958
49084
|
});
|
|
47959
49085
|
}
|
|
47960
49086
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
@@ -49427,8 +50553,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
49427
50553
|
}
|
|
49428
50554
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
49429
50555
|
const rulesyncRules = rulesyncFiles.filter((file) => file instanceof RulesyncRule);
|
|
49430
|
-
const
|
|
49431
|
-
const
|
|
50556
|
+
const alignedRules = this.alignPiContextFile(rulesyncRules);
|
|
50557
|
+
const localRootRules = alignedRules.filter((rule) => rule.getFrontmatter().localRoot);
|
|
50558
|
+
const nonLocalRootRules = alignedRules.filter((rule) => !rule.getFrontmatter().localRoot);
|
|
49432
50559
|
const factory = this.getFactory(this.toolTarget);
|
|
49433
50560
|
const { meta } = factory;
|
|
49434
50561
|
const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
|
|
@@ -49821,6 +50948,44 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
49821
50948
|
return [...targetedRootRules, ...nonRootRules];
|
|
49822
50949
|
}
|
|
49823
50950
|
/**
|
|
50951
|
+
* Pi reads `AGENTS.override.md` *instead of* `AGENTS.md` from a directory, and
|
|
50952
|
+
* non-root Pi rules are folded into whichever file the root emits. A mix of
|
|
50953
|
+
* opted-in and opted-out rules would therefore split the output across both
|
|
50954
|
+
* files and let Pi silently ignore everything in `AGENTS.md`, so the root rule
|
|
50955
|
+
* decides for all of them: its `pi.contextFile` is copied onto the non-root
|
|
50956
|
+
* rules, and the flag set only on a non-root rule is dropped with a warning.
|
|
50957
|
+
*/
|
|
50958
|
+
alignPiContextFile(rules) {
|
|
50959
|
+
if (this.toolTarget !== "pi") return rules;
|
|
50960
|
+
const factory = this.getFactory(this.toolTarget);
|
|
50961
|
+
const targeted = rules.filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
50962
|
+
const rootContextFile = targeted.some((rule) => rule.getFrontmatter().root === true && rule.getFrontmatter().pi?.contextFile === "override") ? "override" : void 0;
|
|
50963
|
+
const mismatched = targeted.filter((rule) => rule.getFrontmatter().pi?.contextFile !== rootContextFile);
|
|
50964
|
+
if (mismatched.length === 0) return rules;
|
|
50965
|
+
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)}`);
|
|
50966
|
+
const mismatchedSet = new Set(mismatched);
|
|
50967
|
+
return rules.map((rule) => {
|
|
50968
|
+
if (!mismatchedSet.has(rule)) return rule;
|
|
50969
|
+
const frontmatter = rule.getFrontmatter();
|
|
50970
|
+
const { contextFile: _dropped, ...pi } = frontmatter.pi ?? {};
|
|
50971
|
+
const nextPi = {
|
|
50972
|
+
...pi,
|
|
50973
|
+
...rootContextFile ? { contextFile: rootContextFile } : {}
|
|
50974
|
+
};
|
|
50975
|
+
return new RulesyncRule({
|
|
50976
|
+
outputRoot: rule.getOutputRoot(),
|
|
50977
|
+
relativeDirPath: rule.getRelativeDirPath(),
|
|
50978
|
+
relativeFilePath: rule.getRelativeFilePath(),
|
|
50979
|
+
frontmatter: {
|
|
50980
|
+
...frontmatter,
|
|
50981
|
+
...Object.keys(nextPi).length > 0 ? { pi: nextPi } : { pi: void 0 }
|
|
50982
|
+
},
|
|
50983
|
+
body: rule.getBody(),
|
|
50984
|
+
validate: false
|
|
50985
|
+
});
|
|
50986
|
+
});
|
|
50987
|
+
}
|
|
50988
|
+
/**
|
|
49824
50989
|
* Implementation of abstract method from FeatureProcessor
|
|
49825
50990
|
* Load tool-specific rule configurations and parse them into ToolRule instances
|
|
49826
50991
|
*/
|
|
@@ -51878,4 +53043,4 @@ async function importChecksCore(params) {
|
|
|
51878
53043
|
//#endregion
|
|
51879
53044
|
export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
|
|
51880
53045
|
|
|
51881
|
-
//# sourceMappingURL=import-
|
|
53046
|
+
//# sourceMappingURL=import-BpKoN2US.js.map
|